From af55069e221621f4d1536eac5dc7cd829652b27d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Mon, 19 Aug 2024 11:10:09 +0000 Subject: [PATCH 01/14] 1196 wip --- .gitignore | 1 + .vscode/launch.json | 4 +- src/app.js | 12 +- src/butler.js | 4 +- .../failed-reload-qscloud copy.handlebars | 70 ++++ .../failed-reload-qscloud-workflow.json | 66 ++++ .../failed-reload-qscloud.handlebars | 70 ++++ src/globals.js | 45 ++- src/lib/mqtt_handlers.js | 94 ++++- src/lib/msteams_notification.js | 5 + src/lib/qscloud/api/app.js | 29 ++ src/lib/qscloud/api/appreloadinfo.js | 132 +++++++ src/lib/qscloud/api/user.js | 30 ++ .../qscloud/mqtt_event_app_reload_finished.js | 182 +++++++++ .../qscloud/msteams_notification_qscloud.js | 352 ++++++++++++++++++ src/lib/{ => qseow}/qliksense_license.js | 38 +- src/lib/{ => qseow}/qliksense_version.js | 4 +- src/lib/{ => qseow}/scriptlog.js | 4 +- src/lib/{ => qseow}/winsvc.js | 0 src/lib/service_monitor.js | 28 +- src/udp/udp_handlers.js | 48 +-- 21 files changed, 1118 insertions(+), 100 deletions(-) create mode 100644 src/config/teams_templates/failed-reload-qscloud copy.handlebars create mode 100644 src/config/teams_templates/failed-reload-qscloud-workflow.json create mode 100644 src/config/teams_templates/failed-reload-qscloud.handlebars create mode 100644 src/lib/qscloud/api/app.js create mode 100644 src/lib/qscloud/api/appreloadinfo.js create mode 100644 src/lib/qscloud/api/user.js create mode 100644 src/lib/qscloud/mqtt_event_app_reload_finished.js create mode 100644 src/lib/qscloud/msteams_notification_qscloud.js rename src/lib/{ => qseow}/qliksense_license.js (98%) rename src/lib/{ => qseow}/qliksense_version.js (96%) rename src/lib/{ => qseow}/scriptlog.js (99%) rename src/lib/{ => qseow}/winsvc.js (100%) diff --git a/.gitignore b/.gitignore index 9afb9d55..c77e2eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ src/config/production_2.yaml src/config/butler-config.yaml src/udp_client/udp-client.exe src/config/production_empty_arrays.yaml +src/config/production_template_filledin.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json index 468882cb..ebd1b7a0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,8 +17,8 @@ }, "args": [ "--configfile", - // "config/production.yaml", - "config/production_template.yaml", + "config/production.yaml", + // "config/production_template.yaml", // "--new-relic-account-name", // "'First NR account'", // "--new-relic-account-id", diff --git a/src/app.js b/src/app.js index 48d2250c..aaacb9a3 100644 --- a/src/app.js +++ b/src/app.js @@ -138,11 +138,11 @@ async function build(opts = {}) { if (globals.config.has('Butler.restServerConfig.enable') && globals.config.get('Butler.restServerConfig.enable') === true) { globals.logger.info( `REST API documentation available at http://${globals.config.get( - 'Butler.restServerConfig.serverHost' - )}:${globals.config.get('Butler.restServerConfig.serverPort')}/documentation` + 'Butler.restServerConfig.serverHost', + )}:${globals.config.get('Butler.restServerConfig.serverPort')}/documentation`, ); globals.logger.info( - '--> Note regarding API docs: If the line above mentions 0.0.0.0, this is the same as ANY server IP address.' + '--> Note regarding API docs: If the line above mentions 0.0.0.0, this is the same as ANY server IP address.', ); globals.logger.info("--> Replace 0.0.0.0 with one of the Butler host's IP addresses to view the API docs page."); } @@ -171,7 +171,7 @@ async function build(opts = {}) { restServer.setErrorHandler((error, request, reply) => { if (error.statusCode === 429) { globals.logger.warn( - `API: Rate limit exceeded for source IP address ${request.ip}. Method=${request.method}, endpoint=${request.url}` + `API: Rate limit exceeded for source IP address ${request.ip}. Method=${request.method}, endpoint=${request.url}`, ); } reply.send(error); @@ -198,7 +198,7 @@ async function build(opts = {}) { servers: [ { url: `http://${globals.config.get('Butler.restServerConfig.serverHost')}:${globals.config.get( - 'Butler.restServerConfig.serverPort' + 'Butler.restServerConfig.serverPort', )}`, }, ], @@ -237,7 +237,7 @@ async function build(opts = {}) { await proxyRestServer.register(FastifyReplyFrom, { // base: `http://localhost:${globals.config.get('Butler.restServerConfig.backgroundServerPort')}`, base: `http://${globals.config.get('Butler.restServerConfig.serverHost')}:${globals.config.get( - 'Butler.restServerConfig.backgroundServerPort' + 'Butler.restServerConfig.backgroundServerPort', )}`, http: true, }); diff --git a/src/butler.js b/src/butler.js index bac239bd..5aa838b4 100644 --- a/src/butler.js +++ b/src/butler.js @@ -30,9 +30,9 @@ const start = async () => { const setupServiceMonitorTimer = (await import('./lib/service_monitor.js')).default; const { setupQlikSenseAccessLicenseMonitor, setupQlikSenseLicenseRelease, setupQlikSenseServerLicenseMonitor } = await import( - './lib/qliksense_license.js' + './lib/qseow/qliksense_license.js' ); - const { setupQlikSenseVersionMonitor } = await import('./lib/qliksense_version.js'); + const { setupQlikSenseVersionMonitor } = await import('./lib/qseow/qliksense_version.js'); // The build function creates a new instance of the App class and returns it. const build = (await import('./app.js')).default; diff --git a/src/config/teams_templates/failed-reload-qscloud copy.handlebars b/src/config/teams_templates/failed-reload-qscloud copy.handlebars new file mode 100644 index 00000000..0ef26d4a --- /dev/null +++ b/src/config/teams_templates/failed-reload-qscloud copy.handlebars @@ -0,0 +1,70 @@ +{ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "themeColor": "0076D7", + "summary": "Qlik Sense reload task failed", + "sections": [{ + "activityTitle": "# Reload of app failed: '{{appName}}'", + "activitySubtitle": "Reload attempt was done on tenant: {{tenantId}}", + "facts": [{ + "name": "App name", + "value": "{{appName}}" + },{ + "name": "App ID", + "value": "{{appId}}" + },{ + "name": "App owner", + "value": "{{appOwnerName}}" + },{ + "name": "App owner email", + "value": "{{appOwnerEmail}}" + }], + "markdown": true + }, { + "facts": [{ + "name": "User starting the reload", + "value": "{{userName}}" + },{ + "name": "Duration", + "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" + },{ + "name": "Execution started", + "value": "{{executionStartTime.startTimeLocal1}}" + },{ + "name": "Execution ended", + "value": "{{executionStopTime.stopTimeLocal1}}" + },{ + "name": "Execution result", + "value": "{{executionStatusText}}" + },{ + "name": "Log message", + "value": "{{logMessage}}" + }], + "markdown": true + },{ + "activityTitle": "Beginning of script log (up to {{scriptLogHeadCount}} lines)", + "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", + "text": "{{scriptLogHead}}", + "markdown": true + },{ + "activityTitle": "End of script log (up to {{scriptLogTailCount}} lines)", + "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", + "text": "{{scriptLogTail}}", + "markdown": true + }], + "potentialAction": [{ + "@type": "OpenUri", + "name": "Qlik Sense QMC", + "targets": [{ + "os": "default", + "uri": "{{qlikSenseQMC}}" + }] + },{ + "@type": "OpenUri", + "name": "Qlik Sense Hub", + "targets": [{ + "os": "default", + "uri": "{{qlikSenseHub}}" + }] + }] +} diff --git a/src/config/teams_templates/failed-reload-qscloud-workflow.json b/src/config/teams_templates/failed-reload-qscloud-workflow.json new file mode 100644 index 00000000..e16da700 --- /dev/null +++ b/src/config/teams_templates/failed-reload-qscloud-workflow.json @@ -0,0 +1,66 @@ +{ + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": null, + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "App reload failed", + "style": "heading", + "wrap": true + }, + + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "Image", + "style": "person", + "url": "https://raw.githubusercontent.com/ptarmiganlabs/butler/master/icon.png", + "altText": "Butler the Bot", + "size": "small" + } + ], + "width": "auto" + }, + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "weight": "bolder", + "text": "Butler the Bot", + "wrap": true + }, + { + "type": "TextBlock", + "spacing": "none", + "text": "Created 2024-08-12", + "isSubtle": true, + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "TextBlock", + "text": "Plain text is ok, but sometimes I long for more..." + } + ] + } + } + ] +} diff --git a/src/config/teams_templates/failed-reload-qscloud.handlebars b/src/config/teams_templates/failed-reload-qscloud.handlebars new file mode 100644 index 00000000..0ef26d4a --- /dev/null +++ b/src/config/teams_templates/failed-reload-qscloud.handlebars @@ -0,0 +1,70 @@ +{ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "themeColor": "0076D7", + "summary": "Qlik Sense reload task failed", + "sections": [{ + "activityTitle": "# Reload of app failed: '{{appName}}'", + "activitySubtitle": "Reload attempt was done on tenant: {{tenantId}}", + "facts": [{ + "name": "App name", + "value": "{{appName}}" + },{ + "name": "App ID", + "value": "{{appId}}" + },{ + "name": "App owner", + "value": "{{appOwnerName}}" + },{ + "name": "App owner email", + "value": "{{appOwnerEmail}}" + }], + "markdown": true + }, { + "facts": [{ + "name": "User starting the reload", + "value": "{{userName}}" + },{ + "name": "Duration", + "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" + },{ + "name": "Execution started", + "value": "{{executionStartTime.startTimeLocal1}}" + },{ + "name": "Execution ended", + "value": "{{executionStopTime.stopTimeLocal1}}" + },{ + "name": "Execution result", + "value": "{{executionStatusText}}" + },{ + "name": "Log message", + "value": "{{logMessage}}" + }], + "markdown": true + },{ + "activityTitle": "Beginning of script log (up to {{scriptLogHeadCount}} lines)", + "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", + "text": "{{scriptLogHead}}", + "markdown": true + },{ + "activityTitle": "End of script log (up to {{scriptLogTailCount}} lines)", + "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", + "text": "{{scriptLogTail}}", + "markdown": true + }], + "potentialAction": [{ + "@type": "OpenUri", + "name": "Qlik Sense QMC", + "targets": [{ + "os": "default", + "uri": "{{qlikSenseQMC}}" + }] + },{ + "@type": "OpenUri", + "name": "Qlik Sense Hub", + "targets": [{ + "os": "default", + "uri": "{{qlikSenseHub}}" + }] + }] +} diff --git a/src/globals.js b/src/globals.js index 40310d60..c8051312 100644 --- a/src/globals.js +++ b/src/globals.js @@ -42,6 +42,9 @@ class Settings { // Add path to package.json file c = upath.join(b, filenamePackage); + + // Set base path of the executable + this.appBasePath = upath.join(b); } else { // Get path to JS file a = fileURLToPath(import.meta.url); @@ -51,6 +54,9 @@ class Settings { // Add path to package.json file c = upath.join(b, '..', filenamePackage); + + // Set base path of the executable + this.appBasePath = upath.join(b, '..'); } const { version } = JSON.parse(readFileSync(c)); @@ -62,26 +68,26 @@ class Settings { .version(this.appVersion) .name('butler') .description( - 'Butler gives superpowers to client-managed Qlik Sense Enterprise on Windows!\nAdvanced reload failure alerts, task scheduler, key-value store, file system access and much more.' + 'Butler gives superpowers to client-managed Qlik Sense Enterprise on Windows!\nAdvanced reload failure alerts, task scheduler, key-value store, file system access and much more.', ) .option('-c, --configfile ', 'path to config file') .addOption(new Option('-l, --loglevel ', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly'])) .option( '--new-relic-account-name ', - 'New Relic account name. Used within Butler to differentiate between different target New Relic accounts' + 'New Relic account name. Used within Butler to differentiate between different target New Relic accounts', ) .option('--new-relic-api-key ', 'insert API key to use with New Relic') .option('--new-relic-account-id ', 'New Relic account ID') .option('--test-email-address
', 'send test email to this address. Used to verify email settings in the config file.') .option( '--test-email-from-address
', - 'send test email from this address. Only relevant when SMTP server allows from address to be set.' + 'send test email from this address. Only relevant when SMTP server allows from address to be set.', ) .option('--no-qs-connection', "don't connect to Qlik Sense server at all. Run in isolated mode") .option( '--api-rate-limit', 'set the API rate limit, per minute. Default is 100 calls/minute. Set to 0 to disable rate limiting.', - 100 + 100, ) .option('--skip-config-verification', 'Disable config file verification', false); @@ -188,9 +194,9 @@ class Settings { winston.format.timestamp(), winston.format.colorize(), winston.format.simple(), - winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`) + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), ), - }) + }), ); if ( @@ -227,7 +233,7 @@ class Settings { level: this.config.get('Butler.logLevel'), datePattern: 'YYYY-MM-DD', maxFiles: '30d', - }) + }), ); } @@ -235,7 +241,7 @@ class Settings { transports: this.logTransports, format: winston.format.combine( winston.format.timestamp(), - winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`) + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), ), }); @@ -251,8 +257,8 @@ class Settings { `New Relic account names/API keys/account IDs (via command line or config file): ${JSON.stringify( this.config.Butler.thirdPartyToolsCredentials.newRelic, null, - 2 - )}` + 2, + )}`, ); // Get certificate file paths for QRS connection @@ -384,15 +390,8 @@ class Settings { // UDP server connection parameters this.udpHost = this.config.get('Butler.udpServerConfig.serverHost'); this.udpServerReloadTaskSocket = null; - // Prepare to listen on port Y for incoming UDP connections regarding failed tasks - // const udpServerReloadTaskSocket = dgram.createSocket({ - // type: 'udp4', - // reuseAddr: true, - // }); this.udpPortTaskFailure = this.config.get('Butler.udpServerConfig.portTaskFailure'); - // this.mqttClient, - // Indicate that we have finished initialising this.initialised = true; @@ -426,12 +425,12 @@ class Settings { if (this.hostInfo.si.os.platform.toLowerCase() !== 'windows') { if (isUncPath(element.fromDirectory) === true) { this.logger.warn( - `FILE COPY CONFIG: UNC paths won't work on non-Windows OSs ("${element.fromDirectory}"). OS is "${this.hostInfo.si.os.platform}".` + `FILE COPY CONFIG: UNC paths won't work on non-Windows OSs ("${element.fromDirectory}"). OS is "${this.hostInfo.si.os.platform}".`, ); } if (isUncPath(element.toDirectory) === true) { this.logger.warn( - `FILE COPY CONFIG: UNC paths won't work on non-Windows OSs ("${element.toDirectory}"). OS is "${this.hostInfo.si.os.platform}".` + `FILE COPY CONFIG: UNC paths won't work on non-Windows OSs ("${element.toDirectory}"). OS is "${this.hostInfo.si.os.platform}".`, ); } } @@ -460,12 +459,12 @@ class Settings { if (this.hostInfo.si.os.platform.toLowerCase() !== 'windows') { if (isUncPath(element.fromDirectory) === true) { this.logger.warn( - `FILE MOVE CONFIG: UNC paths won't work on non-Windows OSs ("${element.fromDirectory}"). OS is "${this.hostInfo.si.os.platform}".` + `FILE MOVE CONFIG: UNC paths won't work on non-Windows OSs ("${element.fromDirectory}"). OS is "${this.hostInfo.si.os.platform}".`, ); } if (isUncPath(element.toDirectory) === true) { this.logger.warn( - `FILE MOVE CONFIG: UNC paths won't work on non-Windows OSs ("${element.toDirectory}"). OS is "${this.hostInfo.si.os.platform}".` + `FILE MOVE CONFIG: UNC paths won't work on non-Windows OSs ("${element.toDirectory}"). OS is "${this.hostInfo.si.os.platform}".`, ); } } @@ -498,7 +497,7 @@ class Settings { if (this.hostInfo.si.os.platform.toLowerCase() !== 'windows') { if (isUncPath(element) === true) { this.logger.warn( - `FILE DELETE CONFIG: UNC paths won't work on non-Windows OSs ("${element}"). OS is "${this.hostInfo.si.os.platform}".` + `FILE DELETE CONFIG: UNC paths won't work on non-Windows OSs ("${element}"). OS is "${this.hostInfo.si.os.platform}".`, ); } } @@ -647,7 +646,7 @@ class Settings { }) .catch((err) => { this.logger.error( - `CONFIG: Error creating new InfluxDB retention policy "${newPolicy.name}"! ${err.stack}` + `CONFIG: Error creating new InfluxDB retention policy "${newPolicy.name}"! ${err.stack}`, ); }); }) diff --git a/src/lib/mqtt_handlers.js b/src/lib/mqtt_handlers.js index abdc28dc..975f7bc8 100644 --- a/src/lib/mqtt_handlers.js +++ b/src/lib/mqtt_handlers.js @@ -7,11 +7,12 @@ import { fileURLToPath } from 'url'; // Load global variables and functions // import { globals, config, logger } from '../globals.js'; import globals from '../globals.js'; -import senseStartTask from '../qrs_util/sense_start_task.js'; +import { handleQlikSenseCloudAppReloadFinished } from './qscloud/mqtt_event_app_reload_finished.js'; const { config, logger } = globals; function mqttInitHandlers() { + // Set up MQTT handlers related to QSEoW try { let mqttClient; let mqttOptions; @@ -73,10 +74,6 @@ function mqttInitHandlers() { mqttOptions = { clientId: config.get('Butler.mqttConfig.azureEventGrid.clientId'), username: config.get('Butler.mqttConfig.azureEventGrid.clientId'), - // protocolVersion: 4, - // protocol: 'mqtts', - // host: config.get('Butler.mqttConfig.brokerHost'), - // port: config.get('Butler.mqttConfig.brokerPort'), key: readCert(keyFile), cert: readCert(certFile), rejectUnauthorized: true, @@ -168,7 +165,92 @@ function mqttInitHandlers() { } } } catch (err) { - logger.error(`MQTT INIT HANDLERS: Could not set up MQTT: ${err}`); + logger.error(`MQTT INIT HANDLERS: Could not set up MQTT for QSEoW: ${err}`); + } + + // ---------------------------- + // Set up MQTT handlers related to Qlik Sense Cloud events + try { + let mqttClient; + let mqttOptions; + + // MQTT is enabled in config file and specifically for Qlik Sense Cloud events + if (config.get('Butler.mqttConfig.enable') && config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.enable')) { + mqttOptions = { + host: config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.host'), + port: config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.port'), + username: config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.username'), + password: config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.password'), + connectTimeout: 30000, // 30 sec connection timeout + }; + + const brokerUrl = `mqtts://${mqttOptions.host}:${mqttOptions.port}`; + logger.verbose(`MQTT INIT HANDLERS: Connecting to MQTT broker for Qlik Sense Cloud events. URL is "${brokerUrl}"`); + + mqttClient = mqtt.connect(brokerUrl, mqttOptions); + globals.mqttClientQlikSenseCloudEvent = mqttClient; + + if (!globals.mqttClientQlikSenseCloudEvent.connected) { + logger.verbose( + `MQTT INIT HANDLERS: Created (but not yet connected) MQTT client for Qlik Sense Cloud events. Host is "${brokerUrl}"` + ); + } + + if (globals.mqttClientQlikSenseCloudEvent) { + // Handler for MQTT connect messages. Called when connection to MQTT broker has been established + globals.mqttClientQlikSenseCloudEvent.on('connect', () => { + try { + logger.info( + `MQTT QS CLOUD EVENT CONNECT: Connected to MQTT broker "${brokerUrl}", with client ID ${globals.mqttClientQlikSenseCloudEvent.options.clientId}` + ); + + // Let the world know that Butler is connected to MQTT, ready to receive Qlik Sense Cloud events + globals.mqttClientQlikSenseCloudEvent.publish( + 'butler/qscloud/event/mqttforward/status', + `Connected to MQTT broker "${brokerUrl}" with client ID ${globals.mqttClientQlikSenseCloudEvent.options.clientId}` + ); + + // Have Butler listen to all messages in the topic subtree specified in the config file + globals.mqttClientQlikSenseCloudEvent.subscribe( + config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.topic.subscriptionRoot') + ); + } catch (err) { + logger.error(`MQTT QS CLOUD EVENT CONNECT: Error=${JSON.stringify(err, null, 2)}`); + } + }); + + // Handler for MQTT messages matching the previously set up subscription + globals.mqttClientQlikSenseCloudEvent.on('message', async (topic, message) => { + try { + const topicStr = topic.toString(); + // message is a JSON string. Convert to object and formatted JSON string + const messageObj = JSON.parse(message.toString()); + const messageStr = JSON.stringify(messageObj, null, 2); + logger.verbose(`MQTT QS CLOUD MESSAGE: Message received. Topic=${topicStr}, Message=${messageStr}`); + + // **MQTT message dispatch** + // Compare the first part of the topic to the configured topic for app reload events + // Event: App reload + // messageObj.eventType should be "com.qlik.v1.app.reload.finished" + if ( + topicStr.startsWith(config.get('Butler.mqttConfig.qlikSenseCloud.event.mqttForward.topic.appReload')) && + messageObj.eventType === 'com.qlik.v1.app.reload.finished' + ) { + logger.debug(`MQTT QS CLOUD EVENT IN: ${messageStr}`); + + // Handle app reload finished event + const appReloadResult = await handleQlikSenseCloudAppReloadFinished(messageObj); + + + } + } catch (err) { + logger.error(`MQTT QS CLOUD MESSAGE: Error=${JSON.stringify(err, null, 2)}`); + } + }); + } + } + } catch (err) { + logger.error(`MQTT INIT HANDLERS: Could not set up MQTT for Qlik Sense Cloud: ${err}`); } } diff --git a/src/lib/msteams_notification.js b/src/lib/msteams_notification.js index f467c5bc..2160a52b 100644 --- a/src/lib/msteams_notification.js +++ b/src/lib/msteams_notification.js @@ -761,3 +761,8 @@ export function sendServiceMonitorNotificationTeams(serviceParams) { globals.logger.verbose(`TEAMS SERVICE MONITOR: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`); }); } + +// Function to send Qlik Sense Cloud app reload failed alert +export function sendQSCloudAppReloadFailedNotificationTeams(reloadParams) { + // +} diff --git a/src/lib/qscloud/api/app.js b/src/lib/qscloud/api/app.js new file mode 100644 index 00000000..cce2d3be --- /dev/null +++ b/src/lib/qscloud/api/app.js @@ -0,0 +1,29 @@ +/* eslint-disable import/prefer-default-export */ +import axios from 'axios'; +import globals from '../../../globals.js'; + +// Function to get info about a specific Qlik Sense Cloud app +// Parameters: +// - appId: Qlik Sense Cloud app ID +export async function getQlikSenseCloudAppInfo(appId) { + try { + // Set up Qlik Sense Cloud API configuration + const axiosConfig = { + url: `/api/v1/apps/${appId}`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const appInfo = JSON.parse(result.data); + + return appInfo; + } catch (err) { + globals.logger.error(`Qlik SENSE CLOUD GET SCRIPT LOG: ${err}`); + return false; + } +} \ No newline at end of file diff --git a/src/lib/qscloud/api/appreloadinfo.js b/src/lib/qscloud/api/appreloadinfo.js new file mode 100644 index 00000000..1109ef26 --- /dev/null +++ b/src/lib/qscloud/api/appreloadinfo.js @@ -0,0 +1,132 @@ +import axios from 'axios'; +import { Duration, DateTime } from 'luxon'; + +import globals from '../../../globals.js'; + +// Function to get script log for a specific Qlik Sense Cloud app reload +// Parameters: +// - appId: Qlik Sense Cloud app ID +// - reloadId: Qlik Sense Cloud reload ID +export async function getQlikSenseCloudAppReloadScriptLog(appId, reloadId, headLineCount, tailLineCount) { + try { + // Set up Qlik Sense Cloud API configuration + const axiosConfig = { + url: `/api/v1/apps/${appId}/reloads/logs/${reloadId}`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const scriptLogFull = result.data.split('\r\n'); + + // Get number of lines in scriptLogFull + const scriptLogLineCount = scriptLogFull.length; + + let scriptLogHead = ''; + let scriptLogTail = ''; + + if (headLineCount > 0) { + scriptLogHead = scriptLogFull.slice(0, headLineCount).join('\r\n'); + } + + if (tailLineCount > 0) { + scriptLogTail = scriptLogFull.slice(Math.max(scriptLogFull.length - tailLineCount, 0)).join('\r\n'); + } + + globals.logger.debug(`QLIK SENSE CLOUD GET SCRIPT LOG: Script log head:\n${scriptLogHead}`); + globals.logger.debug(`QLIK SENSE CLOUD GET SCRIPT LOG: Script log tails:\n${scriptLogTail}`); + + globals.logger.verbose('QLIK SENSE CLOUD GET SCRIPT LOG: Done getting script log'); + + return { + scriptLogFull, + scriptLogSize: scriptLogLineCount, + scriptLogHead, + scriptLogHeadCount: headLineCount, + scriptLogTail, + scriptLogTailCount: tailLineCount, + }; + } catch (err) { + globals.logger.error(`QLIK SENSE CLOUD GET SCRIPT LOG: ${err}`); + return false; + } +} + +// Function to get general info/status/result for a specific Qlik Sense Cloud reload +// Parameters: +// - reloadId: Qlik Sense Cloud reload ID +export async function getQlikSenseCloudAppReloadInfo(reloadId) { + try { + // Set up Qlik Sense Cloud API configuration + const axiosConfig = { + url: `/api/v1/reloads/${reloadId}`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const reloadInfo = JSON.parse(result.data); + + // Reload status lookup, i.e. what initiated the reload. One of + // - hub: one-time reload manually triggered in hub + // - chronos: time based scheduled reload triggered by chronos + // - external = reload triggered via external API request + // - automations = reload triggered in automation + // - data-refresh = reload triggered by refresh of data + + // Get duratiom. Diff between startTime and endTime properties of reloadInfo object + const startTime = DateTime.fromISO(reloadInfo.startTime); + const endTime = DateTime.fromISO(reloadInfo.endTime); + const reloadDuration = endTime.diff(startTime); + + // Add duration as JSON + reloadInfo.executionDuration = reloadDuration.shiftTo('hours', 'minutes', 'seconds').toObject(); + reloadInfo.executionDuration.seconds = Math.floor(reloadInfo.executionDuration.seconds); + + // Add reload created datetime in various formats + let luxonDT = DateTime.fromISO(reloadInfo.creationTime); + reloadInfo.executionCreationTime = { + creationTimeUTC: reloadInfo.creationTime, + creationTimeLocal1: luxonDT.toFormat('yyyy-LL-dd HH:mm:ss'), + creationTimeLocal2: luxonDT.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS), + creationTimeLocal3: luxonDT.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), + creationTimeLocal4: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + creationTimeLocal5: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + }; + + // Add start datetime in various formats + luxonDT = DateTime.fromISO(reloadInfo.startTime); + reloadInfo.executionStartTime = { + startTimeUTC: reloadInfo.startTime, + startTimeLocal1: luxonDT.toFormat('yyyy-LL-dd HH:mm:ss'), + startTimeLocal2: luxonDT.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS), + startTimeLocal3: luxonDT.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), + startTimeLocal4: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + startTimeLocal5: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + }; + + // Add end datetime in various formats + luxonDT = DateTime.fromISO(reloadInfo.endTime); + reloadInfo.executionStopTime = { + stopTimeUTC: reloadInfo.endTime, + stopTimeLocal1: luxonDT.toFormat('yyyy-LL-dd HH:mm:ss'), + stopTimeLocal2: luxonDT.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS), + stopTimeLocal3: luxonDT.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), + stopTimeLocal4: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + stopTimeLocal5: luxonDT.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS), + }; + + return reloadInfo; + } catch (err) { + globals.logger.error(`Qlik SENSE CLOUD GET RELOAD INFO: ${err}`); + return false; + } +} diff --git a/src/lib/qscloud/api/user.js b/src/lib/qscloud/api/user.js new file mode 100644 index 00000000..b505f7ac --- /dev/null +++ b/src/lib/qscloud/api/user.js @@ -0,0 +1,30 @@ +/* eslint-disable import/prefer-default-export */ +import axios from 'axios'; +import globals from '../../../globals.js'; + +// Function to get info about a specific Qlik Sense Cloud user +// Documentation: https://qlik.dev/apis/rest/users/#get-v1-users-userId +// Parameters: +// - userId: Qlik Sense Cloud user ID +export async function getQlikSenseCloudUserInfo(userId) { + try { + // Set up Qlik Sense Cloud API configuration + const axiosConfig = { + url: `/api/v1/users/${userId}`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const appInfo = JSON.parse(result.data); + + return appInfo; + } catch (err) { + globals.logger.error(`Qlik SENSE CLOUD GET SCRIPT LOG: ${err}`); + return false; + } +} \ No newline at end of file diff --git a/src/lib/qscloud/mqtt_event_app_reload_finished.js b/src/lib/qscloud/mqtt_event_app_reload_finished.js new file mode 100644 index 00000000..de959039 --- /dev/null +++ b/src/lib/qscloud/mqtt_event_app_reload_finished.js @@ -0,0 +1,182 @@ +/* eslint-disable import/prefer-default-export */ +import globals from '../../globals.js'; +import { getQlikSenseCloudAppReloadScriptLog, getQlikSenseCloudAppReloadInfo } from './api/appreloadinfo.js'; +import { getQlikSenseCloudAppInfo } from './api/app.js'; +import { sendQlikSenseCloudAppReloadFailureNotificationTeams } from './msteams_notification_qscloud.js'; + +const { config, logger } = globals; + +// Function to handle Qlik Sense Cloud app reload finished event +// Parameters: +// - message: MQTT message object, as sent by Qlik Sense Cloud webhook API +export async function handleQlikSenseCloudAppReloadFinished(message) { + try { + // Make sure eventType is 'com.qlik.v1.app.reload.finished' + if (message.eventType === 'com.qlik.v1.app.reload.finished') { + // What is app reload status? + // Possible values: 'ok', 'error' + if (message.data.status === 'error') { + // Get app ID from message.extensions.topLevelResourceId + const appId = message.extensions.topLevelResourceId; + + // Get info about the app reload from the message sent by Qlik Sense Cloud + const { source, eventType, eventTypeVersion } = message; + const { ownerId, tenantId, userId } = message.extensions; + const { + duration, + endedWithMemoryConstraint, + errors, + isDirectQueryMode, + isPartialReload, + isSessionApp, + isSkipStore, + peakMemoryBytes, + reloadId, + rowLimit, + statements, + status, + usage, + warnings, + } = message.data; + + const appName = message.data.name; + const sizeMemory = message.data?.size?.memory; + + // Type of event that triggered the reload + let reloadTrigger = ''; + + let scriptLog = {}; + let reloadInfo = {}; + let appInfo = {}; + + // App reload did fail. Send enabled notifications/alerts + logger.info(`QLIK SENSE CLOUD: App reload failed. App ID=[${appId}] name="${message.data.name}"`); + + // Are notifications from QS Cloud enabled? + if (globals.config.has('Butler.qlikSenseCloud.enable') && globals.config.get('Butler.qlikSenseCloud.enable') === true) { + // Post to Teams when a task has failed, if enabled + if ( + globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable') && + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable') === + true + ) { + logger.verbose(`QLIK SENSE CLOUD: Sending Teams notification about app reload failure`); + + // Should we get extended info about the event, or go with the basic info provided in the event/MQTT message? + // If extended info is enabled, we need to make API calls to get the extended info + // If extended info is disabled, we can use the basic info provided in the event/MQTT message + if ( + globals.config.has( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly' + ) && + globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly' + ) === false + ) { + // Get extended info about the event + // This includes: + // - Reload script log + // - Reload info + // - App info + + // Script log is available via "GET /v1/apps/{appId}/reloads/logs/{reloadId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-reloads-logs-reloadId + try { + const headLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + ); + + const tailLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + ); + + scriptLog = await getQlikSenseCloudAppReloadScriptLog(appId, reloadId, headLineCount, tailLineCount); + + // If return value is false, the script log could not be obtained + if (scriptLog === false) { + logger.warn( + `QLIK SENSE CLOUD: Could not get app reload script log. App ID="${appId}", reload ID="${reloadId}"` + ); + } else { + logger.verbose( + `QLIK SENSE CLOUD: App reload script log obtained. App ID="${appId}", reload ID="${reloadId}"` + ); + } + logger.debug(`QLIK SENSE CLOUD: App reload script log: ${scriptLog}`); + } catch (err) { + logger.error( + `QLIK SENSE CLOUD: Could not get app reload script log. Error=${JSON.stringify(err, null, 2)}` + ); + } + + // Reload info is available via "GET /v1/reloads/{reloadId}" + // https://qlik.dev/apis/rest/reloads/#get-v1-reloads-reloadId + try { + reloadInfo = await getQlikSenseCloudAppReloadInfo(reloadId); + reloadTrigger = reloadInfo.type; + + logger.verbose(`QLIK SENSE CLOUD: App reload info obtained. App ID="${appId}", reload ID="${reloadId}"`); + logger.debug(`QLIK SENSE CLOUD: App reload info: ${JSON.stringify(reloadInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app reload info. Error=${JSON.stringify(err, null, 2)}`); + } + + // App info is available via "GET /v1/apps/{appId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId + try { + appInfo = await getQlikSenseCloudAppInfo(appId); + + logger.verbose(`QLIK SENSE CLOUD: App info obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App info: ${JSON.stringify(appInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app info. Error=${JSON.stringify(err, null, 2)}`); + } + } else { + // Use the basic info provided in the event/MQTT message + scriptLog = {}; + reloadInfo.appId = appId; + reloadInfo.reloadId = reloadId; + } + + sendQlikSenseCloudAppReloadFailureNotificationTeams({ + tenantId, + userId, + ownerId, + appId, + appName, + reloadTrigger, + + source, + eventType, + eventTypeVersion, + duration, + endedWithMemoryConstraint, + errors, + isDirectQueryMode, + isPartialReload, + isSessionApp, + isSkipStore, + peakMemoryBytes, + reloadId, + rowLimit, + statements, + status, + usage, + warnings, + sizeMemory, + + scriptLog, + reloadInfo, + appInfo, + }); + } + } + } + } + + return true; + } catch (err) { + logger.error(`Qlik Sense Cloud app reload finished event handling error: ${err}`); + return false; + } +} diff --git a/src/lib/qscloud/msteams_notification_qscloud.js b/src/lib/qscloud/msteams_notification_qscloud.js new file mode 100644 index 00000000..01428333 --- /dev/null +++ b/src/lib/qscloud/msteams_notification_qscloud.js @@ -0,0 +1,352 @@ +/* eslint-disable import/prefer-default-export */ +import fs from 'fs'; + +import { Webhook, SimpleTextCard } from 'ms-teams-wrapper'; +import handlebars from 'handlebars'; +import { RateLimiterMemory } from 'rate-limiter-flexible'; + +import globals from '../../globals.js'; +import { getQlikSenseCloudUserInfo } from './api/user.js'; +import { getQlikSenseCloudAppInfo } from './api/app.js'; +// import getAppOwner from '../../qrs_util/get_app_owner.js'; + +let rateLimiterMemoryFailedReloads; + +if (globals.config.has('Butler.teamsNotification.reloadTaskFailure.rateLimit')) { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit'), + }); +} else { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: 300, + }); +} + +function getAppReloadFailedTeamsConfig() { + try { + if (!globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { + // Teams task falure notifications are disabled + globals.logger.error( + "TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Reload failure Teams notifications are disabled in config file - won't send Teams message" + ); + return false; + } + + if ( + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType') !== + 'basic' && + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType') !== + 'formatted' + ) { + // Invalid Teams message type + globals.logger.error( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Invalid Teams message type: ${globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType' + )}` + ); + return false; + } + + return { + webhookUrl: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL'), + messageType: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType'), + templateFile: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile' + ), + + headScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + ), + tailScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + ), + rateLimit: globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit'), + basicMsgTemplate: globals.config.has( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate' + ), + }; + } catch (err) { + globals.logger.error(`TEAMS ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + return false; + } +} + +function getQlikSenseCloudUrls() { + let qmcUrl = ''; + let hubUrl = ''; + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { + qmcUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc'); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { + hubUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub'); + } + + return { + qmcUrl, + hubUrl, + }; +} + +async function sendTeams(teamsWebhookUrl, teamsConfig, templateContext, msgType) { + try { + let compiledTemplate; + let renderedText = null; + let msg = null; + + if (teamsConfig.messageType === 'basic') { + compiledTemplate = handlebars.compile(teamsConfig.basicMsgTemplate); + renderedText = compiledTemplate(templateContext); + + msg = { + '@type': 'MessageCard', + '@context': 'https://schema.org/extensions', + summary: renderedText, + themeColor: '0078D7', + title: renderedText, + potentialAction: [ + { + '@type': 'OpenUri', + name: 'Qlik Sense QMC', + targets: [ + { + os: 'default', + uri: templateContext.qlikSenseQMC, + }, + ], + }, + { + '@type': 'OpenUri', + name: 'Qlik Sense Hub', + targets: [ + { + os: 'default', + uri: templateContext.qlikSenseHub, + }, + ], + }, + ], + }; + } else if (teamsConfig.messageType === 'formatted') { + try { + if (fs.existsSync(teamsConfig.templateFile) === true) { + const template = fs.readFileSync(teamsConfig.templateFile, 'utf8'); + compiledTemplate = handlebars.compile(template); + + if (msgType === 'reload') { + // Escape any back slashes in the script logs + const regExpText = /(?!\\n)\\{1}/gm; + globals.logger.debug(`TEAMS SEND: Script log head escaping: ${regExpText.exec(templateContext.scriptLogHead)}`); + globals.logger.debug(`TEAMS SEND: Script log tail escaping: ${regExpText.exec(templateContext.scriptLogTail)}`); + + templateContext.scriptLogHead = templateContext.scriptLogHead.replace(regExpText, '\\\\'); + templateContext.scriptLogTail = templateContext.scriptLogTail.replace(regExpText, '\\\\'); + } else if (msgType === 'qscloud-app-reload') { + // Escape any back slashes in the script logs + const regExpText = /(?!\\n)\\{1}/gm; + globals.logger.debug(`TEAMS SEND: Script log head escaping: ${regExpText.exec(templateContext.scriptLogHead)}`); + globals.logger.debug(`TEAMS SEND: Script log tail escaping: ${regExpText.exec(templateContext.scriptLogTail)}`); + + templateContext.scriptLogHead = templateContext.scriptLogHead.replace(regExpText, '\\\\'); + templateContext.scriptLogTail = templateContext.scriptLogTail.replace(regExpText, '\\\\'); + } + + renderedText = compiledTemplate(templateContext); + + globals.logger.debug(`TEAMS SEND: Rendered message:\n${renderedText}`); + + // Parse the JSON string to get rid of extra linebreaks etc. + msg = JSON.parse(renderedText); + } else { + globals.logger.error(`TEAMS SEND: Could not open Teams template file ${teamsConfig.templateFile}.`); + } + } catch (err) { + globals.logger.error(`TEAMS SEND: Error processing Teams template file: ${err}`); + } + } + + if (msg !== null) { + const webhook = new Webhook(teamsWebhookUrl, msg); + const res = await webhook.sendMessage(); + + if (res !== undefined) { + globals.logger.debug(`TEAMS SEND: Result from calling TeamsApi.TeamsSend: ${res.statusText} (${res.status}): ${res.data}`); + } + } + } catch (err) { + globals.logger.error(`TEAMS SEND: ${err}`); + } +} + +// Function to send Qlik Sense Cloud app reload failed alert +export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams) { + rateLimiterMemoryFailedReloads + .consume(reloadParams.taskId, 1) + .then(async (rateLimiterRes) => { + try { + globals.logger.info( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting check passed for failed task notification. App name: "${reloadParams.appName}"` + ); + globals.logger.verbose( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"` + ); + + // Make sure Teams sending is enabled in the config file and that we have all required settings + const teamsConfig = getAppReloadFailedTeamsConfig(); + if (teamsConfig === false) { + return 1; + } + + // Get app owner info + const appOwner = await getQlikSenseCloudUserInfo(reloadParams.ownerId); + + // Get script logs, if enabled in the config file + // If the value is false, the script log could not be obtained + let scriptLogData = {}; + + if (reloadParams.scriptLog === false) { + scriptLogData = { + scriptLogFull: [], + scriptLogSize: 0, + scriptLogHead: '', + scriptLogHeadCount: 0, + scriptLogTail: '', + scriptLogTailCount: 0, + }; + } else { + // Reduce script log lines to only the ones we want to send to Teams + scriptLogData.scriptLogHeadCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + ); + scriptLogData.scriptLogTailCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + ); + + if (reloadParams.scriptLog?.scriptLogFull?.length > 0) { + scriptLogData.scriptLogHead = reloadParams.scriptLog.scriptLogFull + .slice(0, reloadParams.scriptLog.scriptLogHeadCount) + .join('\r\n'); + + scriptLogData.scriptLogTail = reloadParams.scriptLog.scriptLogFull + .slice(Math.max(reloadParams.scriptLog.scriptLogFull.length - reloadParams.scriptLog.scriptLogTailCount, 0)) + .join('\r\n'); + } else { + scriptLogData.scriptLogHead = ''; + scriptLogData.scriptLogTail = ''; + } + + // Get length of script log (character count) + scriptLogData.scriptLogSize = reloadParams.scriptLog.scriptLogFull.length; + + globals.logger.debug( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Script log data:\n${JSON.stringify(scriptLogData, null, 2)}` + ); + } + + // Get Sense URLs from config file. Can be used as template fields. + const senseUrls = getQlikSenseCloudUrls(); + + // These are the template fields that can be used in Teams body + const templateContext = { + tenantId: reloadParams.tenantId, + userId: reloadParams.userId, + userName: appOwner === undefined ? 'Unknown' : appOwner.name, + appName: reloadParams.appName, + appId: reloadParams.appId, + errorCode: reloadParams.reloadInfo.errorCode, + errorMessage: reloadParams.reloadInfo.errorMessage, + logMessage: reloadParams.reloadInfo.log + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n\\n') + .replace(/([\t])/gm, '\\t'), + executionDuration: reloadParams.reloadInfo.executionDuration, + executionStartTime: reloadParams.reloadInfo.executionStartTime, + executionStopTime: reloadParams.reloadInfo.executionStopTime, + executionStatusText: reloadParams.reloadInfo.status, + scriptLogSize: scriptLogData.scriptLogSize, + scriptLogHead: scriptLogData.scriptLogHead + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n\\n') + .replace(/([\t])/gm, '\\t'), + scriptLogTail: scriptLogData.scriptLogTail + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n\\n') + .replace(/([\t])/gm, '\\t'), + scriptLogTailCount: scriptLogData.scriptLogTailCount, + scriptLogHeadCount: scriptLogData.scriptLogHeadCount, + qlikSenseQMC: senseUrls.qmcUrl, + qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.name, + appOwnerUserId: appOwner.id, + appOwnerPicture: appOwner.picture, + appOwnerEmail: appOwner.email, + }; + + // Check if script log is longer than 3000 characters. Truncate if so. + if (templateContext.scriptLogHead.length >= 3000) { + globals.logger.warn( + `TEAMS: Script log head field is too long (${templateContext.scriptLogHead.length}), will truncate before posting to Teams.` + ); + templateContext.scriptLogHead = templateContext.scriptLogHead + .replaceAll('&', '&') + .replaceAll('=', '=') + .replaceAll("'", ''') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .slice(0, 2900); + + templateContext.scriptLogHead = templateContext.scriptLogHead + .replaceAll('=', '=') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('&', '&'); + + templateContext.scriptLogHead += '\\n----Script log truncated by Butler----'; + } + + if (templateContext.scriptLogTail.length >= 3000) { + globals.logger.warn( + `TEAMS: Script log head field is too long (${templateContext.scriptLogTail.length}), will truncate before posting to Teams.` + ); + templateContext.scriptLogTail = templateContext.scriptLogTail + .replaceAll('&', '&') + .replaceAll('=', '=') + .replaceAll("'", ''') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .slice(-2900); + + templateContext.scriptLogTail = templateContext.scriptLogTail + .replaceAll('=', '=') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('&', '&'); + + templateContext.scriptLogTail = `----Script log truncated by Butler----\\n${templateContext.scriptLogTail}`; + } + + const { webhookUrl } = teamsConfig; + sendTeams(webhookUrl, teamsConfig, templateContext, 'qscloud-app-reload'); + } catch (err) { + globals.logger.error(`TEAMS ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + } + return true; + }) + .catch((rateLimiterRes) => { + globals.logger.warn( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification Teams for task "${reloadParams.taskName}"` + ); + globals.logger.debug( + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"` + ); + }); +} diff --git a/src/lib/qliksense_license.js b/src/lib/qseow/qliksense_license.js similarity index 98% rename from src/lib/qliksense_license.js rename to src/lib/qseow/qliksense_license.js index b879faa3..092892fc 100644 --- a/src/lib/qliksense_license.js +++ b/src/lib/qseow/qliksense_license.js @@ -1,13 +1,13 @@ import later from '@breejs/later'; import QrsInteract from 'qrs-interact'; -import globals from '../globals.js'; +import globals from '../../globals.js'; import { postQlikSenseLicenseStatusToInfluxDB, postQlikSenseLicenseReleasedToInfluxDB, postQlikSenseServerLicenseStatusToInfluxDB, -} from './post_to_influxdb.js'; -import { callQlikSenseServerLicenseWebhook } from './webhook_notification.js'; +} from '../post_to_influxdb.js'; +import { callQlikSenseServerLicenseWebhook } from '../webhook_notification.js'; // Function to check Qlik Sense server license status async function checkQlikSenseServerLicenseStatus(config, logger) { @@ -152,7 +152,7 @@ async function checkQlikSenseServerLicenseStatus(config, logger) { if (licenseExpired === true) { globals.mqttClient.publish( config.get('Butler.mqttConfig.qlikSenseServerLicenseExpireTopic'), - `Qlik Sense server license expired on ${expiryDateStr}` + `Qlik Sense server license expired on ${expiryDateStr}`, ); } else if (daysUntilExpiry <= config.get('Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays')) { const mqttAlertPayload = `Qlik Sense server license is about to expire in ${daysUntilExpiry} days, on ${expiryDateStr}`; @@ -280,7 +280,7 @@ async function licenseReleaseProfessional(config, logger, qrsInstance) { // Is status code other than 200 or body is empty? if (result1.statusCode !== 200 || !result1.body) { logger.error( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Could not get list of assigned professional licenses. HTTP status code ${result1.statusCode}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Could not get list of assigned professional licenses. HTTP status code ${result1.statusCode}`, ); return false; } @@ -308,18 +308,18 @@ async function licenseReleaseProfessional(config, logger, qrsInstance) { const res = await qrsInstance.Get(`user/${license.user.id}`); if (res.statusCode !== 200 || !res.body) { logger.error( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`, ); return false; } currentUser = res.body; } catch (err) { logger.error( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`, ); if (err.stack) { logger.error( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}`, ); } return false; @@ -476,7 +476,7 @@ async function licenseReleaseProfessional(config, logger, qrsInstance) { // Should currentUser be released? if (!doNotRelease) { logger.info( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseProfessional array` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseProfessional array`, ); releaseProfessional.push({ licenseId: license.id, @@ -486,14 +486,14 @@ async function licenseReleaseProfessional(config, logger, qrsInstance) { }); } else { logger.info( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}`, ); } } } logger.verbose( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Professional licenses to be released: ${JSON.stringify(releaseProfessional, null, 2)}` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Professional licenses to be released: ${JSON.stringify(releaseProfessional, null, 2)}`, ); // Is license release dry-run enabled? If so, do not release any licenses @@ -504,7 +504,7 @@ async function licenseReleaseProfessional(config, logger, qrsInstance) { // eslint-disable-next-line no-restricted-syntax for (const licenseRelease of releaseProfessional) { logger.info( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`, ); // Release license @@ -566,7 +566,7 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance) { // Is status code 200 or body is empty? if (result3.statusCode !== 200 || !result3.body) { logger.error( - `QLIKSENSE LICENSE RELEASE ANALYZER: Could not get list of assigned analyzer licenses. HTTP status code ${result3.statusCode}` + `QLIKSENSE LICENSE RELEASE ANALYZER: Could not get list of assigned analyzer licenses. HTTP status code ${result3.statusCode}`, ); return false; } @@ -594,18 +594,18 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance) { const res = await qrsInstance.Get(`user/${license.user.id}`); if (res.statusCode !== 200 || !res.body) { logger.error( - `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`, ); return false; } currentUser = res.body; } catch (err) { logger.error( - `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`, ); if (err.stack) { logger.error( - `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}` + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}`, ); } return false; @@ -756,7 +756,7 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance) { // Should currentUser be released? if (!doNotRelease) { logger.info( - `QLIKSENSE LICENSE RELEASE ANALYZER: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseAnalyzer array` + `QLIKSENSE LICENSE RELEASE ANALYZER: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseAnalyzer array`, ); releaseAnalyzer.push({ licenseId: license.id, @@ -766,7 +766,7 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance) { }); } else { logger.info( - `QLIKSENSE LICENSE RELEASE ANALYZER: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}` + `QLIKSENSE LICENSE RELEASE ANALYZER: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}`, ); } } @@ -782,7 +782,7 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance) { // eslint-disable-next-line no-restricted-syntax for (const licenseRelease of releaseAnalyzer) { logger.info( - `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`, ); // Release license diff --git a/src/lib/qliksense_version.js b/src/lib/qseow/qliksense_version.js similarity index 96% rename from src/lib/qliksense_version.js rename to src/lib/qseow/qliksense_version.js index 1696ade9..966200c8 100644 --- a/src/lib/qliksense_version.js +++ b/src/lib/qseow/qliksense_version.js @@ -2,8 +2,8 @@ import later from '@breejs/later'; import axios from 'axios'; import https from 'https'; -import globals from '../globals.js'; -import { postQlikSenseVersionToInfluxDB } from './post_to_influxdb.js'; +import globals from '../../globals.js'; +import { postQlikSenseVersionToInfluxDB } from '../post_to_influxdb.js'; // Function to check Qlik Sense version async function checkQlikSenseVersion(config, logger) { diff --git a/src/lib/scriptlog.js b/src/lib/qseow/scriptlog.js similarity index 99% rename from src/lib/scriptlog.js rename to src/lib/qseow/scriptlog.js index e6aee7bb..ac9b5a06 100644 --- a/src/lib/scriptlog.js +++ b/src/lib/qseow/scriptlog.js @@ -4,7 +4,7 @@ import https from 'https'; import { Duration, DateTime } from 'luxon'; import path from 'path'; import fs from 'fs'; -import globals from '../globals.js'; +import globals from '../../globals.js'; const taskStatusLookup = { 0: 'NeverStarted', @@ -301,7 +301,7 @@ export async function failedTaskStoreLogOnDisk(reloadParams) { reloadLogDir, `${reloadParams.logTimeStamp.slice(0, 19).replace(/ /g, '_').replace(/:/g, '-')}_appId=${reloadParams.appId}_taskId=${ reloadParams.taskId - }.log` + }.log`, ); globals.logger.info(`SCRIPTLOG STORE: Writing failed task script log: ${fileName}`); diff --git a/src/lib/winsvc.js b/src/lib/qseow/winsvc.js similarity index 100% rename from src/lib/winsvc.js rename to src/lib/qseow/winsvc.js diff --git a/src/lib/service_monitor.js b/src/lib/service_monitor.js index 0b143c83..1487e75f 100644 --- a/src/lib/service_monitor.js +++ b/src/lib/service_monitor.js @@ -1,7 +1,7 @@ import later from '@breejs/later'; import { createMachine, createActor } from 'xstate'; -import { statusAll, status, details } from './winsvc.js'; +import { statusAll, status, details } from './qseow/winsvc.js'; import globals from '../globals.js'; import newRelic from './incident_mgmt/new_relic_service_monitor.js'; import { sendServiceMonitorWebhook } from './webhook_notification.js'; @@ -43,7 +43,7 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { config.get('Butler.mqttConfig.serviceStoppedTopic').length === 0 ) { logger.verbose( - `"${svc.serviceName}" Windows service on host "${svc.host}" stopped. No MQTT topic defined in config entry "Butler.mqttConfig.serviceStoppedTopic"` + `"${svc.serviceName}" Windows service on host "${svc.host}" stopped. No MQTT topic defined in config entry "Butler.mqttConfig.serviceStoppedTopic"`, ); } else { logger.verbose(`Sending service stopped alert to MQTT for service "${svc.serviceName}" on host "${svc.host}"`); @@ -60,7 +60,7 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { serviceStatus: svc.serviceStatus, servicePrevStatus: svc.prevState, serviceStatusChanged: svc.stateChanged, - }) + }), ); } } else if (svc.serviceStatus === 'RUNNING') { @@ -70,7 +70,7 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { config.get('Butler.mqttConfig.serviceRunningTopic').length === 0 ) { logger.verbose( - `"${svc.serviceName}" Windows service on host "${svc.host}" is running. No MQTT topic defined in config entry "Butler.mqttConfig.serviceRunningTopic"` + `"${svc.serviceName}" Windows service on host "${svc.host}" is running. No MQTT topic defined in config entry "Butler.mqttConfig.serviceRunningTopic"`, ); } else { logger.verbose(`Sending service running message to MQTT for service "${svc.serviceName}" on host "${svc.host}"`); @@ -87,7 +87,7 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { serviceStatus: svc.serviceStatus, servicePrevStatus: svc.prevState, serviceStatusChanged: svc.stateChanged, - }) + }), ); } } @@ -102,7 +102,7 @@ const serviceMonitorMqttSend2 = (config, logger, svc) => { logger.verbose(`"${svc.serviceName}"No MQTT topic defined in config entry "Butler.mqttConfig.serviceStatusTopic"`); } else { logger.verbose( - `MQTT WINDOWS SERVICE STATUS: Sending service status to MQTT: service="${svc.serviceDetails.displayName}", status="${svc.serviceStatus}"` + `MQTT WINDOWS SERVICE STATUS: Sending service status to MQTT: service="${svc.serviceDetails.displayName}", status="${svc.serviceStatus}"`, ); globals.mqttClient.publish( @@ -116,7 +116,7 @@ const serviceMonitorMqttSend2 = (config, logger, svc) => { serviceStartType: svc.serviceDetails.startType, serviceExePath: svc.serviceDetails.exePath, serviceStatus: svc.serviceStatus, - }) + }), ); } }; @@ -141,7 +141,7 @@ const verifyServicesExist = async (config, logger) => { // eslint-disable-next-line no-restricted-syntax for (const service of servicesToCheck) { logger.verbose( - `VERIFY WIN SERVICES EXIST: Checking status of Windows service ${service.name} (="${service.friendlyName}") on host ${host.host}` + `VERIFY WIN SERVICES EXIST: Checking status of Windows service ${service.name} (="${service.friendlyName}") on host ${host.host}`, ); let serviceExists; @@ -150,18 +150,18 @@ const verifyServicesExist = async (config, logger) => { serviceExists = serviceStatusAll.find((svc) => svc.name === service.name); } catch (err) { logger.error( - `VERIFY WIN SERVICES EXIST: Error verifying existence and reachability of service ${service.name} on host ${host.host}: ${err}` + `VERIFY WIN SERVICES EXIST: Error verifying existence and reachability of service ${service.name} on host ${host.host}: ${err}`, ); result = false; } if (serviceExists) { logger.verbose( - `VERIFY WIN SERVICES EXIST: Windows service ${service.name} (="${service.friendlyName}") on host ${host.host} exists.` + `VERIFY WIN SERVICES EXIST: Windows service ${service.name} (="${service.friendlyName}") on host ${host.host} exists.`, ); } else { logger.error( - `VERIFY WIN SERVICES EXIST: Windows service ${service.name} (="${service.friendlyName}") on host ${host.host} does not exist or cannot be reached.` + `VERIFY WIN SERVICES EXIST: Windows service ${service.name} (="${service.friendlyName}") on host ${host.host} does not exist or cannot be reached.`, ); result = false; } @@ -195,7 +195,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { if (svcMonitored === undefined) { logger.error( - `Service ${service.name} (="${service.friendlyName}") on host ${host.host} does not exist or cannot be reached.` + `Service ${service.name} (="${service.friendlyName}") on host ${host.host} does not exist or cannot be reached.`, ); return; } @@ -602,12 +602,12 @@ async function setupServiceMonitorTimer(config, logger) { } } else { logger.error( - 'At least one Windows service does not exist or could not be reached. Monitoring of Windows services is disabled.' + 'At least one Windows service does not exist or could not be reached. Monitoring of Windows services is disabled.', ); } } else { logger.warn( - `Not running on Windows, service monitoring will not work. Current platform is: ${hostInfo.si.os.platform}, distro: ${hostInfo.si.os.distro}, release: ${hostInfo.si.os.release}` + `Not running on Windows, service monitoring will not work. Current platform is: ${hostInfo.si.os.platform}, distro: ${hostInfo.si.os.distro}, release: ${hostInfo.si.os.release}`, ); } } diff --git a/src/udp/udp_handlers.js b/src/udp/udp_handlers.js index dc9d94b7..06cf3738 100644 --- a/src/udp/udp_handlers.js +++ b/src/udp/udp_handlers.js @@ -11,7 +11,7 @@ import { sendReloadTaskAbortedLog, sendReloadTaskAbortedEvent, } from '../lib/incident_mgmt/new_relic.js'; -import { failedTaskStoreLogOnDisk, getScriptLog, getReloadTaskExecutionResults } from '../lib/scriptlog.js'; +import { failedTaskStoreLogOnDisk, getScriptLog, getReloadTaskExecutionResults } from '../lib/qseow/scriptlog.js'; import getTaskTags from '../qrs_util/task_tag_util.js'; import getAppTags from '../qrs_util/app_tag_util.js'; import doesTaskExist from '../qrs_util/does_task_exist.js'; @@ -22,7 +22,7 @@ import { postReloadTaskFailureNotificationInfluxDb, postReloadTaskSuccessNotific // Handler for failed scheduler initiated reloads const schedulerAborted = async (msg) => { globals.logger.verbose( - `TASKABORTED: Received reload aborted UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}` + `TASKABORTED: Received reload aborted UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}`, ); // Get script log for failed reloads. @@ -227,8 +227,8 @@ const schedulerAborted = async (msg) => { } else { globals.logger.warn( `MQTT: MQTT client not connected. Unable to publish message to topic ${globals.config.get( - 'Butler.mqttConfig.taskAbortedTopic' - )}` + 'Butler.mqttConfig.taskAbortedTopic', + )}`, ); } } @@ -256,7 +256,7 @@ const schedulerAborted = async (msg) => { logMessage: msg[10], qs_appTags: appTags, qs_taskTags: taskTags, - }) + }), ); } }; @@ -279,7 +279,7 @@ const schedulerFailed = async (msg) => { } globals.logger.verbose( - `TASKFAILURE: Received reload failed UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}` + `TASKFAILURE: Received reload failed UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}`, ); // First field in message (msg[0]) is message category (this is the modern/recent message format) @@ -516,8 +516,8 @@ const schedulerFailed = async (msg) => { } else { globals.logger.warn( `MQTT: MQTT client not connected. Unable to publish message to topic ${globals.config.get( - 'Butler.mqttConfig.taskFailureTopic' - )}` + 'Butler.mqttConfig.taskFailureTopic', + )}`, ); } } @@ -545,7 +545,7 @@ const schedulerFailed = async (msg) => { logMessage: msg[10], qs_appTags: appTags, qs_taskTags: taskTags, - }) + }), ); } }; @@ -555,7 +555,7 @@ const schedulerFailed = async (msg) => { // -------------------------------------------------------- const schedulerReloadTaskSuccess = async (msg) => { globals.logger.verbose( - `RELOAD TASK SUCCESS: Received reload task success UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}` + `RELOAD TASK SUCCESS: Received reload task success UDP message from scheduler: UDP msg=${msg[0]}, Host=${msg[1]}, App name=${msg[3]}, Task name=${msg[2]}, Log level=${msg[8]}, Log msg=${msg[10]}`, ); const reloadTaskId = msg[5]; @@ -591,7 +591,7 @@ const schedulerReloadTaskSuccess = async (msg) => { reloadTaskId, customPropertyName, customPropertyValue, - globals.logger + globals.logger, ); if (customPropertyValueForTask) { @@ -626,12 +626,12 @@ const schedulerReloadTaskSuccess = async (msg) => { taskInfo.executionDuration.seconds === 0 ) { globals.logger.warn( - `RELOAD TASK SUCCESS: Task info for reload task ${reloadTaskId} retrieved successfully after ${retryCount} attempts, but duration is 0 seconds. This is likely caused by the QRS not having updated the execution details yet.` + `RELOAD TASK SUCCESS: Task info for reload task ${reloadTaskId} retrieved successfully after ${retryCount} attempts, but duration is 0 seconds. This is likely caused by the QRS not having updated the execution details yet.`, ); } globals.logger.debug( - `RELOAD TASK SUCCESS: Task info for reload task ${reloadTaskId} retrieved successfully after ${retryCount} attempts` + `RELOAD TASK SUCCESS: Task info for reload task ${reloadTaskId} retrieved successfully after ${retryCount} attempts`, ); break; } @@ -639,7 +639,7 @@ const schedulerReloadTaskSuccess = async (msg) => { retryCount += 1; globals.logger.verbose( - `RELOAD TASK SUCCESS: Unable to get task info for reload task ${reloadTaskId}. Attempt ${retryCount} of 5. Waiting 1 second before trying again` + `RELOAD TASK SUCCESS: Unable to get task info for reload task ${reloadTaskId}. Attempt ${retryCount} of 5. Waiting 1 second before trying again`, ); // eslint-disable-next-line no-await-in-loop @@ -648,7 +648,7 @@ const schedulerReloadTaskSuccess = async (msg) => { if (!taskInfo) { globals.logger.warn( - `RELOAD TASK SUCCESS: Unable to get task info for reload task ${reloadTaskId}. Not storing task info in InfluxDB` + `RELOAD TASK SUCCESS: Unable to get task info for reload task ${reloadTaskId}. Not storing task info in InfluxDB`, ); return false; } @@ -717,8 +717,8 @@ const udpInitTaskErrorServer = () => { } else { globals.logger.warn( `UDP SERVER INIT: MQTT client not connected. Unable to publish message to topic ${globals.config.get( - 'Butler.mqttConfig.taskFailureServerStatusTopic' - )}` + 'Butler.mqttConfig.taskFailureServerStatusTopic', + )}`, ); } } @@ -738,8 +738,8 @@ const udpInitTaskErrorServer = () => { } else { globals.logger.warn( `UDP SERVER ERROR: MQTT client not connected. Unable to publish message to topic ${globals.config.get( - 'Butler.mqttConfig.taskFailureServerStatusTopic' - )}` + 'Butler.mqttConfig.taskFailureServerStatusTopic', + )}`, ); } } @@ -829,7 +829,7 @@ const udpInitTaskErrorServer = () => { // There should be exactly 11 fields in the message if (msg.length !== 9) { globals.logger.warn( - `UDP HANDLER ENGINE RELOAD FAILED: Invalid number of fields in UDP message. Expected 9, got ${msg.length}.` + `UDP HANDLER ENGINE RELOAD FAILED: Invalid number of fields in UDP message. Expected 9, got ${msg.length}.`, ); globals.logger.warn(`UDP HANDLER ENGINE RELOAD FAILED: Incoming log message was:\n${message.toString()}`); globals.logger.warn(`UDP HANDLER ENGINE RELOAD FAILED: Aborting processing of this message.`); @@ -837,7 +837,7 @@ const udpInitTaskErrorServer = () => { } globals.logger.verbose( - `UDP HANDLER ENGINE RELOAD FAILED: Received reload failed UDP message from engine: Host=${msg[1]}, AppID=${msg[2]}, User directory=${msg[4]}, User=${msg[5]}` + `UDP HANDLER ENGINE RELOAD FAILED: Received reload failed UDP message from engine: Host=${msg[1]}, AppID=${msg[2]}, User directory=${msg[4]}, User=${msg[5]}`, ); } else if (msg[0].toLowerCase() === '/scheduler-reload-failed/') { // Scheduler log appender detecting failed scheduler-started reload @@ -846,7 +846,7 @@ const udpInitTaskErrorServer = () => { // There should be exactly 11 fields in the message if (msg.length !== 11) { globals.logger.warn( - `UDP HANDLER SCHEDULER RELOAD FAILED: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.` + `UDP HANDLER SCHEDULER RELOAD FAILED: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.`, ); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD FAILED: Incoming log message was:\n${message.toString()}`); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD FAILED: Aborting processing of this message.`); @@ -861,7 +861,7 @@ const udpInitTaskErrorServer = () => { // There should be exactly 11 fields in the message if (msg.length !== 11) { globals.logger.warn( - `UDP HANDLER SCHEDULER RELOAD ABORTED: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.` + `UDP HANDLER SCHEDULER RELOAD ABORTED: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.`, ); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD ABORTED: Incoming log message was:\n${message.toString()}`); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD ABORTED: Aborting processing of this message.`); @@ -876,7 +876,7 @@ const udpInitTaskErrorServer = () => { // There should be exactly 11 fields in the message if (msg.length !== 11) { globals.logger.warn( - `UDP HANDLER SCHEDULER RELOAD TASK SUCCESS: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.` + `UDP HANDLER SCHEDULER RELOAD TASK SUCCESS: Invalid number of fields in UDP message. Expected 11, got ${msg.length}.`, ); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD TASK SUCCESS: Incoming log message was:\n${message.toString()}`); globals.logger.warn(`UDP HANDLER SCHEDULER RELOAD TASK SUCCESS: Aborting processing of this message.`); From 523722a00a6a1c7c98ead806a171a6b14a388651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Wed, 28 Aug 2024 11:51:28 +0000 Subject: [PATCH 02/14] feat(qs-cloud): Reload failed alerts to MS Teams Implements parts of #1196 --- .prettierrc | 2 +- src/config/production_template.yaml | 89 ++++ .../failed-reload-qscloud copy.handlebars | 70 ---- .../failed-reload-qscloud-workflow.json | 66 --- .../failed-reload-qscloud.handlebars | 268 +++++++++--- src/lib/assert/assert_config_file.js | 392 ++++++++++++++++++ src/lib/config_obfuscate.js | 78 ++++ .../qscloud/mqtt_event_app_reload_finished.js | 35 +- .../qscloud/msteams_notification_qscloud.js | 29 +- 9 files changed, 812 insertions(+), 217 deletions(-) delete mode 100644 src/config/teams_templates/failed-reload-qscloud copy.handlebars delete mode 100644 src/config/teams_templates/failed-reload-qscloud-workflow.json diff --git a/.prettierrc b/.prettierrc index d053c759..d711a71b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,5 @@ "arrowParens": "always", "bracketSpacing": true, - "useTabs": false + "useTabs": false, } diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 7a8a7e3d..6dd919d3 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -746,6 +746,18 @@ Butler: serviceStatusTopic: qliksense/service_status qlikSenseServerLicenseTopic: qliksense/qliksense_server_license # Topic to which Sense server license info is published qlikSenseServerLicenseExpireTopic: qliksense/qliksense_server_license_expire # Topic to which Sense server license expiration alerts are published + qlikSenseCloud: # MQTT settings for Qlik Sense Cloud integration + event: + mqttForward: # QS Cloud events forwarded to MQTT topics, which Butler will subscribe to + enable: false + broker: # Settings for MQTT broker to which QS Cloud events are forwarded + host: mqttbroker.company.com + port: + username: + password: + topic: + subscriptionRoot: qscloud/# # Topic that Butler will subscribe to + appReload: qscloud/app/reload udpServerConfig: enable: false # Should the UDP server responsible for receving task failure/aborted events be started? @@ -921,6 +933,83 @@ Butler: webhook: # Send service alerts as outbound webhooks/http calls enable: true + qlikSenseCloud: # Settings for Qlik Sense Cloud integration + enable: false + event: + mqtt: # Which QS Cloud tenant should Butler SOS receive events from, in the form of MQTT messages? + tenant: + id: tenant.region.qlikcloud.com + tenantUrl: https://tenant.region.qlikcloud.com + authType: jwt # Authentication type used to connect to the tenant. Valid options are "jwt" + auth: + jwt: + token: # JWT token used to authenticate Butler SOS when connecting to the tenant + # Qlik Sense Cloud related links used in notification messages + qlikSenseUrls: + qmc: + hub: + comment: This is a comment describing the tenant and its settings # Informational only + alert: + # Settings for notifications and messages sent to MS Teams + teamsNotification: + reloadAppFailure: + enable: false + basicContentOnly: false + webhookURL: + + messageType: formatted # formatted / basic + basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"' # Only needed if message type = basic + rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 15 + tailScriptLogLines: 15 + templateFile: /path/to/teams_templates/failed-reload-qscloud-workflow.handlebars + + # Settings for notifications and messages sent to Slack + slackNotification: + reloadAppFailure: + enable: false + basicContentOnly: false + webhookURL: + channel: sense-task-failure # Slack channel to which task failure notifications are sent + messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. + basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"' # Only needed if message type = basic + rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 10 + tailScriptLogLines: 20 + templateFile: /path/to/slack_templates/failed-reload-qscloud.handlebars + fromUser: Qlik Sense + iconEmoji: ':ghost:' + + # Settings needed to send email notifications when for example reload tasks fail. + # Reload failure notifications assume a log appender is configured in Sense AND that the UDP server in Butler is running. + emailNotification: + reloadAppFailure: + enable: false + basicContentOnly: false + appOwnerAlert: + enable: false # Should app owner get notification email (assuming email address is available in Sense user directory) + includeOwner: + includeAll: true # true = Send notification to all app owners except those in exclude list + # false = Send notification to app owners in the include list + user: + - directory: + userId: + excludeOwner: + user: + - directory: + userId: + rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 15 + tailScriptLogLines: 25 + priority: high # high/normal/low + subject: '❌ Qlik Sense reload failed: "{{taskName}}"' + bodyFileDirectory: /path/to//email_templates + htmlTemplateFile: failed-reload-qs-cloud + fromAdress: Qlik Sense (no-reply) + recipients: + - user.joe@company.com + - user.anna@company.com + # Certificates to use when connecting to Sense. Get these from the Certificate Export in QMC. cert: clientCert: diff --git a/src/config/teams_templates/failed-reload-qscloud copy.handlebars b/src/config/teams_templates/failed-reload-qscloud copy.handlebars deleted file mode 100644 index 0ef26d4a..00000000 --- a/src/config/teams_templates/failed-reload-qscloud copy.handlebars +++ /dev/null @@ -1,70 +0,0 @@ -{ - "@type": "MessageCard", - "@context": "https://schema.org/extensions", - "themeColor": "0076D7", - "summary": "Qlik Sense reload task failed", - "sections": [{ - "activityTitle": "# Reload of app failed: '{{appName}}'", - "activitySubtitle": "Reload attempt was done on tenant: {{tenantId}}", - "facts": [{ - "name": "App name", - "value": "{{appName}}" - },{ - "name": "App ID", - "value": "{{appId}}" - },{ - "name": "App owner", - "value": "{{appOwnerName}}" - },{ - "name": "App owner email", - "value": "{{appOwnerEmail}}" - }], - "markdown": true - }, { - "facts": [{ - "name": "User starting the reload", - "value": "{{userName}}" - },{ - "name": "Duration", - "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" - },{ - "name": "Execution started", - "value": "{{executionStartTime.startTimeLocal1}}" - },{ - "name": "Execution ended", - "value": "{{executionStopTime.stopTimeLocal1}}" - },{ - "name": "Execution result", - "value": "{{executionStatusText}}" - },{ - "name": "Log message", - "value": "{{logMessage}}" - }], - "markdown": true - },{ - "activityTitle": "Beginning of script log (up to {{scriptLogHeadCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogHead}}", - "markdown": true - },{ - "activityTitle": "End of script log (up to {{scriptLogTailCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogTail}}", - "markdown": true - }], - "potentialAction": [{ - "@type": "OpenUri", - "name": "Qlik Sense QMC", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseQMC}}" - }] - },{ - "@type": "OpenUri", - "name": "Qlik Sense Hub", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseHub}}" - }] - }] -} diff --git a/src/config/teams_templates/failed-reload-qscloud-workflow.json b/src/config/teams_templates/failed-reload-qscloud-workflow.json deleted file mode 100644 index e16da700..00000000 --- a/src/config/teams_templates/failed-reload-qscloud-workflow.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "message", - "attachments": [ - { - "contentType": "application/vnd.microsoft.card.adaptive", - "contentUrl": null, - "content": { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.3", - "body": [ - { - "type": "TextBlock", - "size": "large", - "weight": "bolder", - "text": "App reload failed", - "style": "heading", - "wrap": true - }, - - { - "type": "ColumnSet", - "columns": [ - { - "type": "Column", - "items": [ - { - "type": "Image", - "style": "person", - "url": "https://raw.githubusercontent.com/ptarmiganlabs/butler/master/icon.png", - "altText": "Butler the Bot", - "size": "small" - } - ], - "width": "auto" - }, - { - "type": "Column", - "items": [ - { - "type": "TextBlock", - "weight": "bolder", - "text": "Butler the Bot", - "wrap": true - }, - { - "type": "TextBlock", - "spacing": "none", - "text": "Created 2024-08-12", - "isSubtle": true, - "wrap": true - } - ], - "width": "stretch" - } - ] - }, - { - "type": "TextBlock", - "text": "Plain text is ok, but sometimes I long for more..." - } - ] - } - } - ] -} diff --git a/src/config/teams_templates/failed-reload-qscloud.handlebars b/src/config/teams_templates/failed-reload-qscloud.handlebars index 0ef26d4a..20bc49a5 100644 --- a/src/config/teams_templates/failed-reload-qscloud.handlebars +++ b/src/config/teams_templates/failed-reload-qscloud.handlebars @@ -1,70 +1,200 @@ { - "@type": "MessageCard", - "@context": "https://schema.org/extensions", - "themeColor": "0076D7", - "summary": "Qlik Sense reload task failed", - "sections": [{ - "activityTitle": "# Reload of app failed: '{{appName}}'", - "activitySubtitle": "Reload attempt was done on tenant: {{tenantId}}", - "facts": [{ - "name": "App name", - "value": "{{appName}}" - },{ - "name": "App ID", - "value": "{{appId}}" - },{ - "name": "App owner", - "value": "{{appOwnerName}}" - },{ - "name": "App owner email", - "value": "{{appOwnerEmail}}" - }], - "markdown": true - }, { - "facts": [{ - "name": "User starting the reload", - "value": "{{userName}}" - },{ - "name": "Duration", - "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" - },{ - "name": "Execution started", - "value": "{{executionStartTime.startTimeLocal1}}" - },{ - "name": "Execution ended", - "value": "{{executionStopTime.stopTimeLocal1}}" - },{ - "name": "Execution result", - "value": "{{executionStatusText}}" - },{ - "name": "Log message", - "value": "{{logMessage}}" - }], - "markdown": true - },{ - "activityTitle": "Beginning of script log (up to {{scriptLogHeadCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogHead}}", - "markdown": true - },{ - "activityTitle": "End of script log (up to {{scriptLogTailCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogTail}}", - "markdown": true - }], - "potentialAction": [{ - "@type": "OpenUri", - "name": "Qlik Sense QMC", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseQMC}}" - }] - },{ - "@type": "OpenUri", - "name": "Qlik Sense Hub", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseHub}}" - }] - }] -} + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": null, + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.3", + "msteams": { + "width": "Full" + }, + "body": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "Qlik Sense Cloud app reload failed", + "style": "heading", + "wrap": true + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "Image", + "style": "person", + "url": "https://raw.githubusercontent.com/ptarmiganlabs/butler/master/icon.png", + "altText": "Butler the Bot", + "size": "medium" + } + ], + "width": "auto" + }, + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "weight": "bolder", + "text": "Butler the Bot", + "wrap": true + }, + { + "type": "TextBlock", + "spacing": "none", + "text": "", + "isSubtle": true, + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "FactSet", + "spacing": "large", + "facts": [ + { + "title": "App name:", + "value": "{{appName}}" + }, + { + "title": "App ID:", + "value": "{{appId}}" + }, + { + "title": "App owner:", + "value": "{{appOwnerName}}" + }, + { + "title": "App owner email:", + "value": "{{appOwnerEmail}}" + }, + { + "title": "Reload trigger:", + "value": "{{reloadTrigger}}" + }, + { + "title": "Reload started:", + "value": "{{executionStartTime.startTimeLocal1}}" + }, + { + "title": "Reload ended:", + "value": "{{executionStopTime.stopTimeLocal1}}" + }, + { + "title": "Duration:", + "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" + }, + { + "title": "Reload ID:", + "value": "{{reloadId}}" + }, + { + "title": "Tenant ID:", + "value": "{{tenantId}}" + }, + { + "title": "Tenant comment:", + "value": "{{tenantComment}}" + } + ] + }, + + { + "type": "ActionSet", + "spacing": "extraLarge", + "separator": true, + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Open QMC", + "tooltip": "Open management console in Qlik Sense Cloud", + "url": "{{qlikSenseQMC}}", + "role": "button" + } + ] + }, + + + { + "type": "Container", + "spacing": "extraLarge", + "style": "emphasis", + "items": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "Details", + "style": "heading" + }, + { + "type": "FactSet", + "separator": true, + "facts": [ + { + "title": "Error message:", + "value": "{{errorMessage}}" + }, + { + "title": "Error code:", + "value": "{{errorCode}}" + }, + { + "title": "Peak memory (bytes):", + "value": "{{peakMemoryBytes}}" + }, + { + "title": "Failed due to memory constraint:", + "value": "{{endedWithMemoryConstraint}}" + }, + { + "title": "Log message:", + "value": "" + } + ] + }, + { + "type": "CodeBlock", + "codeSnippet": "{{logMessage}}" + } + ] + }, + { + "type": "Container", + "spacing": "extraLarge", + "style": "emphasis", + "items": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "End of script log", + "style": "heading" + }, + { + "type": "TextBlock", + "size": "small", + "weight": "normal", + "text": "Last {{scriptLogTailCount}} rows shown. The script log contains {{scriptLogSize}} rows in total.", + "style": "heading" + }, + { + "type": "CodeBlock", + "codeSnippet": "{{scriptLogTail}}" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js index 84270834..2d50127a 100644 --- a/src/lib/assert/assert_config_file.js +++ b/src/lib/assert/assert_config_file.js @@ -4179,6 +4179,398 @@ export const configFileStructureAssert = async (config, logger) => { configFileCorrect = false; } + // QS Cloud settings + if (!config.has('Butler.qlikSenseCloud.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.id')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.id"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.authType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.authType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.comment')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.comment"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"', + ); + configFileCorrect = false; + } + + if ( + !config.has( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll', + ) + ) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll"', + ); + configFileCorrect = false; + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties: + // - directory: 'string' + // - userId: 'string' + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) { + const users = config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user', + ); + + if (users) { + if (!Array.isArray(users)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user" must be an array', + ); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + } + }); + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user"', + ); + configFileCorrect = false; + } + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties: + // - directory: 'string' + // - userId: 'string' + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) { + const users = config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user', + ); + + if (users) { + if (!Array.isArray(users)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user" must be an array', + ); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + } + }); + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user"', + ); + configFileCorrect = false; + } + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress"', + ); + configFileCorrect = false; + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) { + const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" must be an array', + ); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[${index}]" must be a string`, + ); + configFileCorrect = false; + } + }); + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', + ); + configFileCorrect = false; + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', + ); + configFileCorrect = false; + } + // Butler.udpServerConfig if (!config.has('Butler.udpServerConfig.enable')) { logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.enable"'); diff --git a/src/lib/config_obfuscate.js b/src/lib/config_obfuscate.js index 126d37e4..ae5ca322 100644 --- a/src/lib/config_obfuscate.js +++ b/src/lib/config_obfuscate.js @@ -126,6 +126,17 @@ function configObfuscate(config) { obfuscatedConfig.Butler.mqttConfig.azureEventGrid.clientId = obfuscatedConfig.Butler.mqttConfig.azureEventGrid.clientId.substring(0, 3) + '*'.repeat(10); + // Obfuscate Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.host, keep first 3 chars, mask the rest with * + obfuscatedConfig.Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.host = + obfuscatedConfig.Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.host.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.auth.username, keep first 3 chars, mask the rest with * + obfuscatedConfig.Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.username = + obfuscatedConfig.Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.username.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.auth.password, keep first 0 chars, mask the rest with * + obfuscatedConfig.Butler.mqttConfig.qlikSenseCloud.event.mqttForward.broker.password = '*'.repeat(10); + // Obfuscate Butler.udpServerConfig.serverHost, keep first 3 chars, mask the rest with * obfuscatedConfig.Butler.udpServerConfig.serverHost = obfuscatedConfig.Butler.udpServerConfig.serverHost.substring(0, 3) + '*'.repeat(10); @@ -141,6 +152,73 @@ function configObfuscate(config) { host: element.host.substring(0, 3) + '*'.repeat(10), })); + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.id, keep first 3 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.id = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.id.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl, keep first 10 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token, keep first 5 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token.substring(0, 5) + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc, keep first 10 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub, keep first 10 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL, keep first 10 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL.substring(0, 10) + + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppAFailure.webhookURL, keep first 10 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL.substring(0, 10) + + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user + // This is an array of objects, each object has a property "directory" and "userId". + // Obfuscateboth properties, keep first 0 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user?.map( + (element) => ({ + ...element, + directory: '*'.repeat(10), + userId: '*'.repeat(10), + }), + ); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user + // This is an array of objects, each object has a property "directory" and "userId". + // Obfuscateboth properties, keep first 0 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user?.map( + (element) => ({ + ...element, + directory: '*'.repeat(10), + userId: '*'.repeat(10), + }), + ); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress, keep first 5 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress.substring(0, 5) + + '*'.repeat(10); + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients + // This is an array of strings, each string is an email address + // Obfuscate each email address, keep first 5 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients?.map( + (element) => element.substring(0, 5) + '*'.repeat(10), + ); + // Obfuscate Butler.configEngine.host, keep first 3 chars, mask the rest with * obfuscatedConfig.Butler.configEngine.host = obfuscatedConfig.Butler.configEngine.host.substring(0, 3) + '*'.repeat(10); diff --git a/src/lib/qscloud/mqtt_event_app_reload_finished.js b/src/lib/qscloud/mqtt_event_app_reload_finished.js index de959039..3fd522d9 100644 --- a/src/lib/qscloud/mqtt_event_app_reload_finished.js +++ b/src/lib/qscloud/mqtt_event_app_reload_finished.js @@ -39,6 +39,26 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { warnings, } = message.data; + // Create array of monitored tenants (id + free text comment) + const monitoredTenants = []; + monitoredTenants.push({ + id: config.get('Butler.qlikSenseCloud.event.mqtt.tenant.id'), + comment: config.get('Butler.qlikSenseCloud.event.mqtt.tenant.comment'), + }); + + // Make sure incoming message is from a Qlik Sense Cloud tenant that we are monitoring + // If the tenant ID is not in the list of monitored tenants, skip the event and warn about it + // Valid tenant ID is defined in the Butler configuration file + if (!monitoredTenants.some((tenant) => tenant.id === tenantId)) { + logger.warn( + `QLIK SENSE CLOUD: Incoming MQTT event from Qlik Sense Cloud, but tenant ID "${tenantId}" is not defined in the Butler configuration file. Skipping event.`, + ); + return false; + } + + // Get tenant comment based on tenant ID + const tenantComment = monitoredTenants.find((tenant) => tenant.id === tenantId).comment; + const appName = message.data.name; const sizeMemory = message.data?.size?.memory; @@ -67,10 +87,10 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { // If extended info is disabled, we can use the basic info provided in the event/MQTT message if ( globals.config.has( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly', ) && globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly', ) === false ) { // Get extended info about the event @@ -83,11 +103,11 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-reloads-logs-reloadId try { const headLineCount = globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines', ); const tailLineCount = globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines', ); scriptLog = await getQlikSenseCloudAppReloadScriptLog(appId, reloadId, headLineCount, tailLineCount); @@ -95,17 +115,17 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { // If return value is false, the script log could not be obtained if (scriptLog === false) { logger.warn( - `QLIK SENSE CLOUD: Could not get app reload script log. App ID="${appId}", reload ID="${reloadId}"` + `QLIK SENSE CLOUD: Could not get app reload script log. App ID="${appId}", reload ID="${reloadId}"`, ); } else { logger.verbose( - `QLIK SENSE CLOUD: App reload script log obtained. App ID="${appId}", reload ID="${reloadId}"` + `QLIK SENSE CLOUD: App reload script log obtained. App ID="${appId}", reload ID="${reloadId}"`, ); } logger.debug(`QLIK SENSE CLOUD: App reload script log: ${scriptLog}`); } catch (err) { logger.error( - `QLIK SENSE CLOUD: Could not get app reload script log. Error=${JSON.stringify(err, null, 2)}` + `QLIK SENSE CLOUD: Could not get app reload script log. Error=${JSON.stringify(err, null, 2)}`, ); } @@ -140,6 +160,7 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { sendQlikSenseCloudAppReloadFailureNotificationTeams({ tenantId, + tenantComment, userId, ownerId, appId, diff --git a/src/lib/qscloud/msteams_notification_qscloud.js b/src/lib/qscloud/msteams_notification_qscloud.js index 01428333..f6653e75 100644 --- a/src/lib/qscloud/msteams_notification_qscloud.js +++ b/src/lib/qscloud/msteams_notification_qscloud.js @@ -252,15 +252,34 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams // These are the template fields that can be used in Teams body const templateContext = { tenantId: reloadParams.tenantId, + tenantComment: reloadParams.tenantComment, userId: reloadParams.userId, userName: appOwner === undefined ? 'Unknown' : appOwner.name, - appName: reloadParams.appName, appId: reloadParams.appId, + appName: reloadParams.appName, + + reloadTrigger: reloadParams.reloadTrigger, + source: reloadParams.source, + eventType: reloadParams.eventType, + eventTypeVersion: reloadParams.eventTypeVersion, + endedWithMemoryConstraint: reloadParams.endedWithMemoryConstraint, + isDirectQueryMode: reloadParams.isDirectQueryMode, + isPartialReload: reloadParams.isPartialReload, + isSessionApp: reloadParams.isSessionApp, + isSkipStore: reloadParams.isSkipStore, + peakMemoryBytes: reloadParams.peakMemoryBytes, + reloadId: reloadParams.reloadId, + rowLimit: reloadParams.rowLimit, + statements: reloadParams.statements, + status: reloadParams.status, + usageDuration: reloadParams.duration, + sizeMemoryBytes: reloadParams.sizeMemory, + errorCode: reloadParams.reloadInfo.errorCode, errorMessage: reloadParams.reloadInfo.errorMessage, logMessage: reloadParams.reloadInfo.log .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), executionDuration: reloadParams.reloadInfo.executionDuration, executionStartTime: reloadParams.reloadInfo.executionStartTime, @@ -269,11 +288,11 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams scriptLogSize: scriptLogData.scriptLogSize, scriptLogHead: scriptLogData.scriptLogHead .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTail: scriptLogData.scriptLogTail .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTailCount: scriptLogData.scriptLogTailCount, scriptLogHeadCount: scriptLogData.scriptLogHeadCount, @@ -283,6 +302,8 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams appOwnerUserId: appOwner.id, appOwnerPicture: appOwner.picture, appOwnerEmail: appOwner.email, + + }; // Check if script log is longer than 3000 characters. Truncate if so. From fa35d91dc6993c2c32722425b1d3a66fef7f54df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Thu, 29 Aug 2024 07:51:48 +0000 Subject: [PATCH 03/14] feat(qs-cloud): Reload failed alerts to Slack Implements parts of #1196 --- .../failed-reload-qscloud.handlebars | 160 +++++++ .../qscloud/mqtt_event_app_reload_finished.js | 122 ++++- .../qscloud/msteams_notification_qscloud.js | 97 ++-- src/lib/qscloud/slack_notification_qscloud.js | 415 ++++++++++++++++++ src/lib/slack_api.js | 19 + src/lib/slack_notification.js | 2 +- 6 files changed, 768 insertions(+), 47 deletions(-) create mode 100644 src/config/slack_templates/failed-reload-qscloud.handlebars create mode 100644 src/lib/qscloud/slack_notification_qscloud.js diff --git a/src/config/slack_templates/failed-reload-qscloud.handlebars b/src/config/slack_templates/failed-reload-qscloud.handlebars new file mode 100644 index 00000000..450f53db --- /dev/null +++ b/src/config/slack_templates/failed-reload-qscloud.handlebars @@ -0,0 +1,160 @@ +{ + "blocks": [{ + "type": "header", + "text": { + "type": "plain_text", + "text": "Qlik Sense Cloud app reload failed" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*App name:*\n{{appName}}" + }, + { + "type": "mrkdwn", + "text": "*App ID:*\n{{{appId}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner:*\n{{{appOwnerName}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner email:*\n{{{appOwnerEmail}}}" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Reload started:*\n{{executionStartTime.startTimeLocal1}}" + }, + { + "type": "mrkdwn", + "text": "*Reload ended:*\n{{executionStopTime.stopTimeLocal1}}" + }, + { + "type": "mrkdwn", + "text": "*Duration:*\n{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" + }, + { + "type": "mrkdwn", + "text": " " + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Trigger:*\n{{reloadTrigger}}" + }, + { + "type": "mrkdwn", + "text": "*Reload ID:*\n{{reloadId}}" + }, + { + "type": "mrkdwn", + "text": "*Tenant ID:*\n{{tenantId}}" + }, + { + "type": "mrkdwn", + "text": "*Tenant comment:*\n{{tenantComment}}" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Error message:*\n{{errorMessage}}" + }, + { + "type": "mrkdwn", + "text": "*Error code:*\n{{errorCode}}" + }, + { + "type": "mrkdwn", + "text": "*Peak memory bytes:*\n{{peakMemoryBytes}}" + }, + { + "type": "mrkdwn", + "text": "*Failed due to memory constraint:*\n{{endedWithMemoryConstraint}}" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "emoji": true, + "text": "Open QMC" + }, + "style": "primary", + "url": "{{qlikSenseQMC}}" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "emoji": true, + "text": "Open Hub" + }, + "style": "primary", + "url": "{{qlikSenseHub}}" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "```{{ logMessage }}```" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "The script log contains {{scriptLogSize}} rows in total. Here are the first lines:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "```{{{ scriptLogHead }}}```" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Here are the last {{scriptLogTailCount}} rows:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "```{{{ scriptLogTail }}}```" + } + } + ] +} diff --git a/src/lib/qscloud/mqtt_event_app_reload_finished.js b/src/lib/qscloud/mqtt_event_app_reload_finished.js index 3fd522d9..a82bb8e4 100644 --- a/src/lib/qscloud/mqtt_event_app_reload_finished.js +++ b/src/lib/qscloud/mqtt_event_app_reload_finished.js @@ -3,6 +3,7 @@ import globals from '../../globals.js'; import { getQlikSenseCloudAppReloadScriptLog, getQlikSenseCloudAppReloadInfo } from './api/appreloadinfo.js'; import { getQlikSenseCloudAppInfo } from './api/app.js'; import { sendQlikSenseCloudAppReloadFailureNotificationTeams } from './msteams_notification_qscloud.js'; +import { sendQlikSenseCloudAppReloadFailureNotificationSlack } from './slack_notification_qscloud.js'; const { config, logger } = globals; @@ -74,7 +75,7 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { // Are notifications from QS Cloud enabled? if (globals.config.has('Butler.qlikSenseCloud.enable') && globals.config.get('Butler.qlikSenseCloud.enable') === true) { - // Post to Teams when a task has failed, if enabled + // Post to Teams when an app reload has failed, if enabled if ( globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable') && globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable') === @@ -191,6 +192,125 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { appInfo, }); } + + // Post to Slack when an app reload has failed, if enabled + if ( + globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable') && + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable') === + true + ) { + logger.verbose(`QLIK SENSE CLOUD: Sending Slack notification about app reload failure`); + + // Should we get extended info about the event, or go with the basic info provided in the event/MQTT message? + // If extended info is enabled, we need to make API calls to get the extended info + // If extended info is disabled, we can use the basic info provided in the event/MQTT message + if ( + globals.config.has( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly', + ) && + globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly', + ) === false + ) { + // Get extended info about the event + // This includes: + // - Reload script log + // - Reload info + // - App info + + // Script log is available via "GET /v1/apps/{appId}/reloads/logs/{reloadId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-reloads-logs-reloadId + try { + const headLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines', + ); + + const tailLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines', + ); + + scriptLog = await getQlikSenseCloudAppReloadScriptLog(appId, reloadId, headLineCount, tailLineCount); + + // If return value is false, the script log could not be obtained + if (scriptLog === false) { + logger.warn( + `QLIK SENSE CLOUD: Could not get app reload script log. App ID="${appId}", reload ID="${reloadId}"`, + ); + } else { + logger.verbose( + `QLIK SENSE CLOUD: App reload script log obtained. App ID="${appId}", reload ID="${reloadId}"`, + ); + } + logger.debug(`QLIK SENSE CLOUD: App reload script log: ${scriptLog}`); + } catch (err) { + logger.error( + `QLIK SENSE CLOUD: Could not get app reload script log. Error=${JSON.stringify(err, null, 2)}`, + ); + } + + // Reload info is available via "GET /v1/reloads/{reloadId}" + // https://qlik.dev/apis/rest/reloads/#get-v1-reloads-reloadId + try { + reloadInfo = await getQlikSenseCloudAppReloadInfo(reloadId); + reloadTrigger = reloadInfo.type; + + logger.verbose(`QLIK SENSE CLOUD: App reload info obtained. App ID="${appId}", reload ID="${reloadId}"`); + logger.debug(`QLIK SENSE CLOUD: App reload info: ${JSON.stringify(reloadInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app reload info. Error=${JSON.stringify(err, null, 2)}`); + } + + // App info is available via "GET /v1/apps/{appId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId + try { + appInfo = await getQlikSenseCloudAppInfo(appId); + + logger.verbose(`QLIK SENSE CLOUD: App info obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App info: ${JSON.stringify(appInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app info. Error=${JSON.stringify(err, null, 2)}`); + } + } else { + // Use the basic info provided in the event/MQTT message + scriptLog = {}; + reloadInfo.appId = appId; + reloadInfo.reloadId = reloadId; + } + + // Send Slack notification + sendQlikSenseCloudAppReloadFailureNotificationSlack({ + tenantId, + tenantComment, + userId, + ownerId, + appId, + appName, + reloadTrigger, + + source, + eventType, + eventTypeVersion, + duration, + endedWithMemoryConstraint, + errors, + isDirectQueryMode, + isPartialReload, + isSessionApp, + isSkipStore, + peakMemoryBytes, + reloadId, + rowLimit, + statements, + status, + usage, + warnings, + sizeMemory, + + scriptLog, + reloadInfo, + appInfo, + }); + } } } } diff --git a/src/lib/qscloud/msteams_notification_qscloud.js b/src/lib/qscloud/msteams_notification_qscloud.js index f6653e75..fc76af4a 100644 --- a/src/lib/qscloud/msteams_notification_qscloud.js +++ b/src/lib/qscloud/msteams_notification_qscloud.js @@ -29,7 +29,7 @@ function getAppReloadFailedTeamsConfig() { if (!globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { // Teams task falure notifications are disabled globals.logger.error( - "TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Reload failure Teams notifications are disabled in config file - won't send Teams message" + "TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Reload failure Teams notifications are disabled in config file - won't send Teams message", ); return false; } @@ -43,8 +43,8 @@ function getAppReloadFailedTeamsConfig() { // Invalid Teams message type globals.logger.error( `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Invalid Teams message type: ${globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType' - )}` + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType', + )}`, ); return false; } @@ -53,18 +53,18 @@ function getAppReloadFailedTeamsConfig() { webhookUrl: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL'), messageType: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType'), templateFile: globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile', ), headScriptLogLines: globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines', ), tailScriptLogLines: globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines', ), - rateLimit: globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit'), - basicMsgTemplate: globals.config.has( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate' + rateLimit: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit'), + basicMsgTemplate: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate', ), }; } catch (err) { @@ -102,31 +102,40 @@ async function sendTeams(teamsWebhookUrl, teamsConfig, templateContext, msgType) renderedText = compiledTemplate(templateContext); msg = { - '@type': 'MessageCard', - '@context': 'https://schema.org/extensions', - summary: renderedText, - themeColor: '0078D7', - title: renderedText, - potentialAction: [ + type: 'message', + attachments: [ { - '@type': 'OpenUri', - name: 'Qlik Sense QMC', - targets: [ - { - os: 'default', - uri: templateContext.qlikSenseQMC, - }, - ], - }, - { - '@type': 'OpenUri', - name: 'Qlik Sense Hub', - targets: [ - { - os: 'default', - uri: templateContext.qlikSenseHub, - }, - ], + contentType: 'application/vnd.microsoft.card.adaptive', + contentUrl: null, + content: { + $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', + type: 'AdaptiveCard', + version: '1.3', + body: [ + { + type: 'TextBlock', + size: 'large', + weight: 'bolder', + text: renderedText, + style: 'heading', + wrap: true, + }, + { + type: 'ActionSet', + spacing: 'extraLarge', + separator: true, + actions: [ + { + type: 'Action.OpenUrl', + title: 'Open QMC', + tooltip: 'Open management console in Qlik Sense Cloud', + url: templateContext.qlikSenseQMC, + role: 'button', + }, + ], + }, + ], + }, }, ], }; @@ -184,14 +193,14 @@ async function sendTeams(teamsWebhookUrl, teamsConfig, templateContext, msgType) // Function to send Qlik Sense Cloud app reload failed alert export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams) { rateLimiterMemoryFailedReloads - .consume(reloadParams.taskId, 1) + .consume(reloadParams.reloadId, 1) .then(async (rateLimiterRes) => { try { globals.logger.info( - `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting check passed for failed task notification. App name: "${reloadParams.appName}"` + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting check passed for failed task notification. App name: "${reloadParams.appName}"`, ); globals.logger.verbose( - `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"` + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, ); // Make sure Teams sending is enabled in the config file and that we have all required settings @@ -219,10 +228,10 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams } else { // Reduce script log lines to only the ones we want to send to Teams scriptLogData.scriptLogHeadCount = globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines', ); scriptLogData.scriptLogTailCount = globals.config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines' + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines', ); if (reloadParams.scriptLog?.scriptLogFull?.length > 0) { @@ -242,7 +251,7 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams scriptLogData.scriptLogSize = reloadParams.scriptLog.scriptLogFull.length; globals.logger.debug( - `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Script log data:\n${JSON.stringify(scriptLogData, null, 2)}` + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Script log data:\n${JSON.stringify(scriptLogData, null, 2)}`, ); } @@ -302,14 +311,12 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams appOwnerUserId: appOwner.id, appOwnerPicture: appOwner.picture, appOwnerEmail: appOwner.email, - - }; // Check if script log is longer than 3000 characters. Truncate if so. if (templateContext.scriptLogHead.length >= 3000) { globals.logger.warn( - `TEAMS: Script log head field is too long (${templateContext.scriptLogHead.length}), will truncate before posting to Teams.` + `TEAMS: Script log head field is too long (${templateContext.scriptLogHead.length}), will truncate before posting to Teams.`, ); templateContext.scriptLogHead = templateContext.scriptLogHead .replaceAll('&', '&') @@ -333,7 +340,7 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams if (templateContext.scriptLogTail.length >= 3000) { globals.logger.warn( - `TEAMS: Script log head field is too long (${templateContext.scriptLogTail.length}), will truncate before posting to Teams.` + `TEAMS: Script log head field is too long (${templateContext.scriptLogTail.length}), will truncate before posting to Teams.`, ); templateContext.scriptLogTail = templateContext.scriptLogTail .replaceAll('&', '&') @@ -364,10 +371,10 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams }) .catch((rateLimiterRes) => { globals.logger.warn( - `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification Teams for task "${reloadParams.taskName}"` + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification Teams for app [${reloadParams.appId}] "${reloadParams.appName}"`, ); globals.logger.debug( - `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"` + `TEAMS ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, ); }); } diff --git a/src/lib/qscloud/slack_notification_qscloud.js b/src/lib/qscloud/slack_notification_qscloud.js new file mode 100644 index 00000000..f5335144 --- /dev/null +++ b/src/lib/qscloud/slack_notification_qscloud.js @@ -0,0 +1,415 @@ +import fs from 'fs'; +import handlebars from 'handlebars'; +import { RateLimiterMemory } from 'rate-limiter-flexible'; + +import globals from '../../globals.js'; +import slackSend from '../slack_api.js'; +import { getQlikSenseCloudUserInfo } from './api/user.js'; +import { getQlikSenseCloudAppInfo } from './api/app.js'; +// import getAppOwner from '../../qrs_util/get_app_owner.js'; + +let rateLimiterMemoryFailedReloads; + +if (globals.config.has('Butler.slackNotification.reloadTaskFailure.rateLimit')) { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit'), + }); +} else { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: 300, + }); +} + +function getAppReloadFailedSlackConfig() { + try { + if (!globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { + // Slack task falure notifications are disabled + globals.logger.error( + "SLACK ALERT - QS CLOUD APP RELOAD FAILED: Reload failure Slack notifications are disabled in config file - won't send Slack message", + ); + return false; + } + + if ( + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType') !== + 'basic' && + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType') !== + 'formatted' + ) { + // Invalid Slack message type + globals.logger.error( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Invalid Slack message type: ${globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType', + )}`, + ); + return false; + } + + if ( + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType') === 'basic' + ) { + // Basic formatting. Make sure requried parameters are present + if (!globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate')) { + // No message text in config file. + globals.logger.error('SLACK ALERT - QS CLOUD APP RELOAD FAILED: No message text in config file.'); + return false; + } + } else if ( + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType') === + 'formatted' + ) { + // Extended formatting using Slack blocks. Make sure requried parameters are present + if (!globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile')) { + globals.logger.error('SLACK ALERT - QS CLOUD APP RELOAD FAILED: Message template file not specified in config file.'); + return false; + } + } + + return { + webhookUrl: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL'), + messageType: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType'), + templateFile: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile', + ), + + headScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines', + ), + tailScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines', + ), + fromUser: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser'), + iconEmoji: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji'), + rateLimit: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit'), + basicMsgTemplate: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate', + ), + channel: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.channel'), + }; + } catch (err) { + globals.logger.error(`SLACK ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + return false; + } +} + +function getQlikSenseCloudUrls() { + let qmcUrl = ''; + let hubUrl = ''; + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { + qmcUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc'); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { + hubUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub'); + } + + return { + qmcUrl, + hubUrl, + }; +} + +async function sendSlack(slackConfig, templateContext, msgType) { + try { + let compiledTemplate; + let renderedText = null; + let slackMsg = null; + const msg = slackConfig; + + if (slackConfig.messageType === 'basic') { + compiledTemplate = handlebars.compile(slackConfig.basicMsgTemplate); + renderedText = compiledTemplate(templateContext); + + slackMsg = { + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: renderedText, + }, + }, + { + type: 'divider', + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + emoji: true, + text: 'Open QMC', + }, + style: 'primary', + url: templateContext.qlikSenseQMC, + }, + { + type: 'button', + text: { + type: 'plain_text', + emoji: true, + text: 'Open Hub', + }, + style: 'primary', + url: templateContext.qlikSenseHub, + }, + ], + }, + ], + }; + } else if (slackConfig.messageType === 'formatted') { + try { + if (fs.existsSync(slackConfig.templateFile) === true) { + // Read template file + const template = fs.readFileSync(slackConfig.templateFile, 'utf8'); + + // Compile the template + compiledTemplate = handlebars.compile(template); + + if (msgType === 'reload') { + // Escape any back slashes in the script logs + const regExpText = /(?!\\n)\\{1}/gm; + globals.logger.debug(`SLACK SEND: Script log head escaping: ${regExpText.exec(templateContext.scriptLogHead)}`); + globals.logger.debug(`SLACK SEND: Script log tail escaping: ${regExpText.exec(templateContext.scriptLogTail)}`); + + templateContext.scriptLogHead = templateContext.scriptLogHead.replace(regExpText, '\\\\'); + templateContext.scriptLogTail = templateContext.scriptLogTail.replace(regExpText, '\\\\'); + } else if (msgType === 'qscloud-app-reload') { + // Escape any back slashes in the script logs + const regExpText = /(?!\\n)\\{1}/gm; + globals.logger.debug(`SLACK SEND: Script log head escaping: ${regExpText.exec(templateContext.scriptLogHead)}`); + globals.logger.debug(`SLACK SEND: Script log tail escaping: ${regExpText.exec(templateContext.scriptLogTail)}`); + + templateContext.scriptLogHead = templateContext.scriptLogHead.replace(regExpText, '\\\\'); + templateContext.scriptLogTail = templateContext.scriptLogTail.replace(regExpText, '\\\\'); + } + + slackMsg = compiledTemplate(templateContext); + + globals.logger.debug(`SLACK SEND: Rendered message:\n${slackMsg}`); + } else { + globals.logger.error(`SLACK SEND: Could not open Slack template file ${slackConfig.templateFile}.`); + } + } catch (err) { + globals.logger.error(`SLACK SEND: Error processing Slack template file: ${err}`); + } + } + + if (slackMsg !== null) { + slackConfig.text = slackMsg; + const res = await slackSend(slackConfig, globals.logger); + + if (res !== undefined) { + globals.logger.debug(`SLACK SEND: Result from calling SlackApi.SlackSend: ${res.statusText} (${res.status}): ${res.data}`); + } + } + } catch (err) { + globals.logger.error(`SLACK SEND: ${err}`); + } +} + +// Function to send Qlik Sense Cloud app reload failed alert +export function sendQlikSenseCloudAppReloadFailureNotificationSlack(reloadParams) { + rateLimiterMemoryFailedReloads + .consume(reloadParams.reloadId, 1) + .then(async (rateLimiterRes) => { + try { + globals.logger.info( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting check passed for failed task notification. App name: "${reloadParams.appName}"`, + ); + globals.logger.verbose( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, + ); + + // Make sure Slack sending is enabled in the config file and that we have all required settings + const slackConfig = getAppReloadFailedSlackConfig(); + if (slackConfig === false) { + return 1; + } + + // Get app owner info + const appOwner = await getQlikSenseCloudUserInfo(reloadParams.ownerId); + + // Get script logs, if enabled in the config file + // If the value is false, the script log could not be obtained + let scriptLogData = {}; + + if (reloadParams.scriptLog === false) { + scriptLogData = { + scriptLogFull: [], + scriptLogSize: 0, + scriptLogHead: '', + scriptLogHeadCount: 0, + scriptLogTail: '', + scriptLogTailCount: 0, + }; + } else { + // Reduce script log lines to only the ones we want to send to Slack + scriptLogData.scriptLogHeadCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines', + ); + scriptLogData.scriptLogTailCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines', + ); + + if (reloadParams.scriptLog?.scriptLogFull?.length > 0) { + // Get length of script log (character count) + scriptLogData.scriptLogSize = reloadParams.scriptLog.scriptLogFull.length; + + // Get the first and last n lines of the script log + scriptLogData.scriptLogHead = reloadParams.scriptLog.scriptLogFull + .slice(0, reloadParams.scriptLog.scriptLogHeadCount) + .join('\r\n'); + + scriptLogData.scriptLogTail = reloadParams.scriptLog.scriptLogFull + .slice(Math.max(reloadParams.scriptLog.scriptLogFull.length - reloadParams.scriptLog.scriptLogTailCount, 0)) + .join('\r\n'); + } else { + scriptLogData.scriptLogHead = ''; + scriptLogData.scriptLogTail = ''; + scriptLogData.scriptLogSize = 0; + } + + globals.logger.debug( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Script log data:\n${JSON.stringify(scriptLogData, null, 2)}`, + ); + } + + // Get Sense URLs from config file. Can be used as template fields. + const senseUrls = getQlikSenseCloudUrls(); + + // These are the template fields that can be used in Slack body + const templateContext = { + tenantId: reloadParams.tenantId, + tenantComment: reloadParams.tenantComment, + userId: reloadParams.userId, + userName: appOwner === undefined ? 'Unknown' : appOwner.name, + appId: reloadParams.appId, + appName: reloadParams.appName, + + reloadTrigger: reloadParams.reloadTrigger, + source: reloadParams.source, + eventType: reloadParams.eventType, + eventTypeVersion: reloadParams.eventTypeVersion, + endedWithMemoryConstraint: reloadParams.endedWithMemoryConstraint, + isDirectQueryMode: reloadParams.isDirectQueryMode, + isPartialReload: reloadParams.isPartialReload, + isSessionApp: reloadParams.isSessionApp, + isSkipStore: reloadParams.isSkipStore, + peakMemoryBytes: reloadParams.peakMemoryBytes, + reloadId: reloadParams.reloadId, + rowLimit: reloadParams.rowLimit, + statements: reloadParams.statements, + status: reloadParams.status, + usageDuration: reloadParams.duration, + sizeMemoryBytes: reloadParams.sizeMemory, + + errorCode: reloadParams.reloadInfo.errorCode, + errorMessage: reloadParams.reloadInfo.errorMessage, + logMessage: reloadParams.reloadInfo.log + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n') + .replace(/([\t])/gm, '\\t'), + executionDuration: reloadParams.reloadInfo.executionDuration, + executionStartTime: reloadParams.reloadInfo.executionStartTime, + executionStopTime: reloadParams.reloadInfo.executionStopTime, + executionStatusText: reloadParams.reloadInfo.status, + scriptLogSize: scriptLogData.scriptLogSize, + scriptLogHead: scriptLogData.scriptLogHead + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n') + .replace(/([\t])/gm, '\\t'), + scriptLogTail: scriptLogData.scriptLogTail + .replace(/([\r])/gm, '') + .replace(/([\n])/gm, '\\n') + .replace(/([\t])/gm, '\\t'), + scriptLogTailCount: scriptLogData.scriptLogTailCount, + scriptLogHeadCount: scriptLogData.scriptLogHeadCount, + qlikSenseQMC: senseUrls.qmcUrl, + qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.name, + appOwnerUserId: appOwner.id, + appOwnerPicture: appOwner.picture, + appOwnerEmail: appOwner.email, + }; + + // Replace all single and double quotes in scriptLogHead and scriptLogTail with escaped dittos + // This is needed to avoid breaking the Slack message JSON + const regExpSingle = /'/gm; + const regExpDouble = /"/gm; + templateContext.scriptLogHead = templateContext.scriptLogHead.replace(regExpSingle, "'").replace(regExpDouble, "\\'"); + templateContext.scriptLogTail = templateContext.scriptLogTail.replace(regExpSingle, "'").replace(regExpDouble, "\\'"); + + // Replace all single and double quotes in logMessage with escaped ditto + // This is needed to avoid breaking the Slack message JSON + templateContext.logMessage = templateContext.logMessage.replace(regExpSingle, "\\'").replace(regExpDouble, "\\'"); + + // Check if script log is longer than 3000 characters. Truncate if so. + if (templateContext.scriptLogHead.length >= 3000) { + globals.logger.warn( + `SLACK: Script log head field is too long (${templateContext.scriptLogHead.length}), will truncate before posting to Slack.`, + ); + templateContext.scriptLogHead = templateContext.scriptLogHead + .replaceAll('&', '&') + .replaceAll('=', '=') + .replaceAll("'", ''') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .slice(0, 2900); + + templateContext.scriptLogHead = templateContext.scriptLogHead + .replaceAll('=', '=') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('&', '&'); + + templateContext.scriptLogHead += '\\n----Script log truncated by Butler----'; + } + + if (templateContext.scriptLogTail.length >= 3000) { + globals.logger.warn( + `SLACK: Script log head field is too long (${templateContext.scriptLogTail.length}), will truncate before posting to Slack.`, + ); + templateContext.scriptLogTail = templateContext.scriptLogTail + .replaceAll('&', '&') + .replaceAll('=', '=') + .replaceAll("'", ''') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .slice(-2900); + + templateContext.scriptLogTail = templateContext.scriptLogTail + .replaceAll('=', '=') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('&', '&'); + + templateContext.scriptLogTail = `----Script log truncated by Butler----\\n${templateContext.scriptLogTail}`; + } + + sendSlack(slackConfig, templateContext, 'qscloud-app-reload'); + } catch (err) { + globals.logger.error(`SLACK ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + } + return true; + }) + .catch((rateLimiterRes) => { + globals.logger.warn( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification Slack for app [${reloadParams.appId}] "${reloadParams.appName}"`, + ); + globals.logger.debug( + `SLACK ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, + ); + }); +} diff --git a/src/lib/slack_api.js b/src/lib/slack_api.js index 3f58c115..3e96a6a7 100644 --- a/src/lib/slack_api.js +++ b/src/lib/slack_api.js @@ -1,5 +1,24 @@ import axios from 'axios'; +/** + * + * @param {*} slackConfig + * slackConfig = { + * webhookUrl: 'https://hooks.slack.com/services/...', + * messageType: 'basic', // basic, formatted, restmsg + * templateFile: 'slack_template.json', + * headScriptLogLines: 'Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit. Nullam nec purus.', + * tailScriptLogLines: 'Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit. Nullam nec purus.', + * fromUser: 'MyBot', + * iconEmoji: ':ghost:', + * rateLimit: 30, + * basicMsgTemplate: 'abc123...', + * channel: '#general', + * text: 'Hello, world!' + * } + * @param {*} logger + * @returns + */ async function slackSend(slackConfig, logger) { // TODO Sanity check Slack config if (slackConfig.text === undefined) { diff --git a/src/lib/slack_notification.js b/src/lib/slack_notification.js index 6a4d7088..5bfccb2a 100644 --- a/src/lib/slack_notification.js +++ b/src/lib/slack_notification.js @@ -545,7 +545,7 @@ export function sendReloadTaskFailureNotificationSlack(reloadParams) { appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; - // Replace all single and dpouble quotes in scriptLogHead and scriptLogTail with escaped dittos + // Replace all single and double quotes in scriptLogHead and scriptLogTail with escaped dittos // This is needed to avoid breaking the Slack message JSON const regExpSingle = /'/gm; const regExpDouble = /"/gm; From 1d4a62d38f5ae8382c2b483c1af1faf4cf40902f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Thu, 26 Sep 2024 07:58:17 +0000 Subject: [PATCH 04/14] feat(qs-cloud): Reload failed alerts to email Partly implements #1196 --- .../failed-reload-qscloud.handlebars | 175 ++++++++ src/config/production_template.yaml | 20 +- .../failed-reload-qscloud.handlebars | 2 +- src/lib/assert/assert_config_file.js | 42 +- src/lib/config_obfuscate.js | 6 +- src/lib/guid_util.js | 22 + src/lib/qscloud/api/app.js | 84 +++- src/lib/qscloud/email_notification_qscloud.js | 384 ++++++++++++++++++ .../qscloud/mqtt_event_app_reload_finished.js | 257 +++++++++++- .../qscloud/msteams_notification_qscloud.js | 43 +- src/lib/qscloud/slack_notification_qscloud.js | 43 +- src/lib/qscloud/util.js | 19 + src/lib/smtp.js | 4 +- 13 files changed, 1004 insertions(+), 97 deletions(-) create mode 100644 src/config/email_templates/failed-reload-qscloud.handlebars create mode 100644 src/lib/guid_util.js create mode 100644 src/lib/qscloud/email_notification_qscloud.js create mode 100644 src/lib/qscloud/util.js diff --git a/src/config/email_templates/failed-reload-qscloud.handlebars b/src/config/email_templates/failed-reload-qscloud.handlebars new file mode 100644 index 00000000..74c9cf5d --- /dev/null +++ b/src/config/email_templates/failed-reload-qscloud.handlebars @@ -0,0 +1,175 @@ +

Qlik Sense Cloud app reload failed

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ App name
+ {{appName}} +
+ App ID
+ {{appId}} +
+ App description
+ {{appDescription}} +
+ Link to app
+ {{appUrl}} +
+ App owner
+ {{appOwnerName}} +
+ App owner email
+ {{appOwnerEmail}} +
+ Tenant ID
+ {{tenantId}} +
+ Tenant comment
+ {{tenantComment}} +

+ Reload started
+ {{executionStartTime.startTimeLocal1}} +
+ Reload ended
+ {{executionStopTime.stopTimeLocal1}} +
+ Duration
+ {{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds +
+
+ Trigger
+ {{reloadTrigger}} +
+ Reload ID
+ {{reloadId}} +

+ Error message
+ {{errorMessage}} +
+ Error code
+ {{errorCode}} +
+ Execution result
+ {{executionStatusText}} +
+
+
+ Peak memory bytes
+ {{peakMemoryBytes}} +
+ Failed due to memory constraint
+ {{endedWithMemoryConstraint}} +

+ Qlik Sense QMC + Qlik Sense Hub + Open app +

+ Log message +
+
{{logMessage}}
+

+ The script log contains {{scriptLogSize}} rows in total. Here are the first ones: +
+
{{scriptLogHead}}
+

+ Here are the last {{scriptLogTailCount}} rows: +
+
{{scriptLogTail}}
+
diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 6dd919d3..4cd2aed1 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -984,31 +984,29 @@ Butler: # Reload failure notifications assume a log appender is configured in Sense AND that the UDP server in Butler is running. emailNotification: reloadAppFailure: - enable: false - basicContentOnly: false + enable: false # Enable/disable app reload failed notifications via email appOwnerAlert: - enable: false # Should app owner get notification email (assuming email address is available in Sense user directory) + enable: false # Should app owner get notification email (assuming email address is available in Sense)? includeOwner: includeAll: true # true = Send notification to all app owners except those in exclude list # false = Send notification to app owners in the include list - user: - - directory: - userId: + user: # Array of app owner email addresses that should get notifications + # - email: anna@somecompany.com + # - email: joe@somecompany.com excludeOwner: user: - - directory: - userId: + # - email: daniel@somecompany.com rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 15 tailScriptLogLines: 25 priority: high # high/normal/low subject: '❌ Qlik Sense reload failed: "{{taskName}}"' bodyFileDirectory: /path/to//email_templates - htmlTemplateFile: failed-reload-qs-cloud + htmlTemplateFile: failed-reload-qscloud fromAdress: Qlik Sense (no-reply) recipients: - - user.joe@company.com - - user.anna@company.com + # - emma@somecompany.com + # - patrick@somecompany.com # Certificates to use when connecting to Sense. Get these from the Certificate Export in QMC. cert: diff --git a/src/config/slack_templates/failed-reload-qscloud.handlebars b/src/config/slack_templates/failed-reload-qscloud.handlebars index 450f53db..fbcdec7e 100644 --- a/src/config/slack_templates/failed-reload-qscloud.handlebars +++ b/src/config/slack_templates/failed-reload-qscloud.handlebars @@ -132,7 +132,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "The script log contains {{scriptLogSize}} rows in total. Here are the first lines:" + "text": "The script log contains {{scriptLogSize}} rows in total. Here are the first ones:" } }, { diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js index 2d50127a..19d52b14 100644 --- a/src/lib/assert/assert_config_file.js +++ b/src/lib/assert/assert_config_file.js @@ -4367,13 +4367,6 @@ export const configFileStructureAssert = async (config, logger) => { configFileCorrect = false; } - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly"', - ); - configFileCorrect = false; - } - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) { logger.error( 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"', @@ -4393,8 +4386,7 @@ export const configFileStructureAssert = async (config, logger) => { } // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties: - // - directory: 'string' - // - userId: 'string' + // - email: 'string' if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) { const users = config.get( 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user', @@ -4414,15 +4406,9 @@ export const configFileStructureAssert = async (config, logger) => { ); configFileCorrect = false; } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + if (!Object.prototype.hasOwnProperty.call(user, 'email')) { logger.error( - `ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, + `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, ); configFileCorrect = false; } @@ -4438,8 +4424,7 @@ export const configFileStructureAssert = async (config, logger) => { } // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties: - // - directory: 'string' - // - userId: 'string' + // - email: 'string' if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) { const users = config.get( 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user', @@ -4459,15 +4444,9 @@ export const configFileStructureAssert = async (config, logger) => { ); configFileCorrect = false; } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + if (!Object.prototype.hasOwnProperty.call(user, 'email')) { logger.error( - `ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, ); configFileCorrect = false; } @@ -4531,14 +4510,15 @@ export const configFileStructureAssert = async (config, logger) => { configFileCorrect = false; } - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress')) { + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress')) { logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress"', + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress"', ); configFileCorrect = false; } // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings + // It is ok for the array to be empty if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) { const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients'); @@ -4558,6 +4538,10 @@ export const configFileStructureAssert = async (config, logger) => { } }); } + } else if (recipients === null) { + logger.warn( + 'ASSERT CONFIG: No recipients defined for Qlik Sense cloud alert emails, "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" is empty.', + ); } else { logger.error( 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', diff --git a/src/lib/config_obfuscate.js b/src/lib/config_obfuscate.js index ae5ca322..5dae1df1 100644 --- a/src/lib/config_obfuscate.js +++ b/src/lib/config_obfuscate.js @@ -206,9 +206,9 @@ function configObfuscate(config) { }), ); - // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress, keep first 5 chars, mask the rest with * - obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress = - obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress.substring(0, 5) + + // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress, keep first 5 chars, mask the rest with * + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress = + obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress.substring(0, 5) + '*'.repeat(10); // Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients diff --git a/src/lib/guid_util.js b/src/lib/guid_util.js new file mode 100644 index 00000000..c002aeb9 --- /dev/null +++ b/src/lib/guid_util.js @@ -0,0 +1,22 @@ +import globals from '../globals.js'; + +// Function to verify if a string is a valid GUID +// Parameters: +// - guid: string to verify +// Returns: +// - true if guid is valid, false otherwise +export const verifyGuid = (guid) => { + try { + // Construct a new RegExp object matching guids + const guidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/; + + if (guidRegExp.test(guid) === true) { + globals.logger.verbose(`GUID VERIFY: GUID is valid: ${guid}`); + return true; + } + globals.logger.warn(`GUID VERIFY: GUID not valid: ${guid}`); + } catch (err) { + globals.logger.error(`GUID VERIFY: Error verifying GUID: ${err}`); + } + return false; +}; diff --git a/src/lib/qscloud/api/app.js b/src/lib/qscloud/api/app.js index cce2d3be..84adc517 100644 --- a/src/lib/qscloud/api/app.js +++ b/src/lib/qscloud/api/app.js @@ -1,12 +1,19 @@ /* eslint-disable import/prefer-default-export */ import axios from 'axios'; import globals from '../../../globals.js'; +import { verifyGuid } from '../../guid_util.js'; // Function to get info about a specific Qlik Sense Cloud app // Parameters: // - appId: Qlik Sense Cloud app ID export async function getQlikSenseCloudAppInfo(appId) { try { + // Make sure appId is valid GUID. If not, log error and return false + if (verifyGuid(appId) === false) { + globals.logger.error(`SENSE CLOUD GET APP ITEMS: Invalid appId: ${appId}`); + return false; + } + // Set up Qlik Sense Cloud API configuration const axiosConfig = { url: `/api/v1/apps/${appId}`, @@ -22,8 +29,83 @@ export async function getQlikSenseCloudAppInfo(appId) { const appInfo = JSON.parse(result.data); return appInfo; + } catch (err) { + globals.logger.error(`SENSE CLOUD GET APP INFO: ${err}`); + return false; + } +} + +// Function to get metadata for a specific Qlik Sense Cloud app +// Parameters: +// - appId: Qlik Sense Cloud app ID +export async function getQlikSenseCloudAppMetadata(appId) { + try { + // Make sure appId is valid GUID. If not, log error and return false + if (verifyGuid(appId) === false) { + globals.logger.error(`SENSE CLOUD GET APP ITEMS: Invalid appId: ${appId}`); + return false; + } + + // Set up Qlik Sense Cloud API configuration + const axiosConfig = { + url: `/api/v1/apps/${appId}/data/metadata`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const appMetadata = JSON.parse(result.data); + + return appMetadata; + } catch (err) { + globals.logger.error(`SENSE CLOUD GET APP METADATA: ${err}`); + return false; + } +} + +// Function to get app items for a specific Qlik Sense Cloud app +// Parameters: +// - appId: Qlik Sense Cloud app ID +export async function getQlikSenseCloudAppItems(appId) { + try { + // Make sure appId is valid GUID. If not, log error and return false + if (verifyGuid(appId) === false) { + globals.logger.error(`SENSE CLOUD GET APP ITEMS: Invalid appId: ${appId}`); + return false; + } + + // Set up Qlik Sense Cloud API configuration + // Query parameters are: + // - resourceType = 'app' + // - resourceId = + // - noActions = true + // - limit = 100 + const axiosConfig = { + url: `/api/v1/items`, + method: 'get', + baseURL: `${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')}`, + params: { + resourceType: 'app', + resourceId: appId, + noActions: true, + limit: 100, + }, + headers: { + Authorization: `Bearer ${globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')}`, + }, + timeout: 30000, + responseType: 'application/json', + }; + const result = await axios.request(axiosConfig); + const appItems = JSON.parse(result.data); + + return appItems; } catch (err) { globals.logger.error(`Qlik SENSE CLOUD GET SCRIPT LOG: ${err}`); return false; } -} \ No newline at end of file +} diff --git a/src/lib/qscloud/email_notification_qscloud.js b/src/lib/qscloud/email_notification_qscloud.js new file mode 100644 index 00000000..a1443f1a --- /dev/null +++ b/src/lib/qscloud/email_notification_qscloud.js @@ -0,0 +1,384 @@ +import fs from 'fs'; +import handlebars from 'handlebars'; +import { RateLimiterMemory } from 'rate-limiter-flexible'; + +import globals from '../../globals.js'; +import { getQlikSenseCloudUserInfo } from './api/user.js'; +import { getQlikSenseCloudAppInfo } from './api/app.js'; +import { getQlikSenseCloudUrls } from './util.js'; +import { sendEmail, isSmtpConfigOk } from '../smtp.js'; + +let rateLimiterMemoryFailedReloads; +let emailConfig; + +if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit')) { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit'), + }); +} else { + rateLimiterMemoryFailedReloads = new RateLimiterMemory({ + points: 1, + duration: 300, + }); +} + +function getAppReloadFailedEmailConfig() { + try { + // Is email alerts on failed reloads enabled? + if (!globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { + globals.logger.error( + 'EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Email alerts on failed reloads are disabled in the config file.', + ); + return false; + } + + // Get app owner alert settings. + const appOwnerAlert = JSON.parse( + JSON.stringify( + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert'), + ), + ); + + return { + emailAlertByTagEnable: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.enable', + ), + emailAlertByTagName: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.tag', + ), + appOwnerAlert, + rateLimit: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit'), + headScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines', + ), + tailScriptLogLines: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines', + ), + priority: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority'), + subject: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject'), + bodyFileDirectory: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory', + ), + htmlTemplateFile: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile', + ), + fromAddress: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress'), + globalSendList: globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients', + ), + }; + } catch (err) { + globals.logger.error(`EMAIL ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + return false; + } +} + +// Function to send Qlik Sense Cloud app reload failed alert as email +export function sendQlikSenseCloudAppReloadFailureNotificationEmail(reloadParams) { + rateLimiterMemoryFailedReloads + .consume(reloadParams.reloadId, 1) + .then(async (rateLimiterRes) => { + try { + globals.logger.info( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting check passed for failed task notification. App name: "${reloadParams.appName}"`, + ); + globals.logger.verbose( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, + ); + + // Logic for determining if alert email should be sent or not + // 1. If config setting Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enabled is false, do not send email + // 2. If config setting Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.enable is true, + // ...only send email if the app has the tag specified in Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.tag + // + // Logic for determining list of email recipients + // 1. Should alert emails be sent for all failed reload tasks? + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.enable = true => only send email if app has tag + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.enable = false => send email for all failed reloads + // 1. Yes: Add system-wide list of recipients to send list. This list is defined in Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[] + // 2. No: Does the app whose reload failed have a tag that enables alert emails? + // 1. Yes: Add system-wide list of recipients to send list. This list is defined in Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[] + // 2. No: Don't add recpients to send list + // 2. Should app owners get alerts? Determined by Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable, which is a boolean + // 1. Yes: Should *all* app owners get alerts? Determined by Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll, which is a boolean + // 1. Yes: Add app owner's email address to app owner send list + // 2. No: Is the app owner included in the Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[] array? + // 1. Yes: Add app owner's email address to app owner send list + // 2. No: Don't add app owner's email address to app owner send list + // 2. Is there an app owner exclusion list? Determined by Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.[] + // 1. Yes: Is the app owner's email address in the exclusion list? + // 1. Yes: Remove the app owner's email address from the app owner send list (if it's there) + // 3. Add app owner send list to main send list + // 4. Remove any duplicate email addresses from the main send list + + // Make sure email sending is enabled in the config file and that we have all required settings + emailConfig = getAppReloadFailedEmailConfig(); + if (emailConfig === false) { + return 1; + } + + // Get send list based on logic described above + let globalSendList = []; + + // Get recipients based on app tags (or for all failed reloads) + if (emailConfig.emailAlertByTagEnable === false) { + // Email alerts are enabled for all failed app reloads, not just those with a specific tag set + if (emailConfig?.globalSendList?.length < 0) { + // Add global send list from YAML config file to main send list + globalSendList.push(...emailConfig.globalSendList); + } + } else { + // Check if app has the tag that enables email alerts. If not found, do not add anything to the main send list + // The app tag names are in reloadParams.meta.tags[].name + const alertTag = emailConfig.emailAlertByTagName; + const appTags = reloadParams.appItems.meta.tags; + const appHasAlertTag = appTags.find((tag) => tag.name === alertTag); + + if (appTags === undefined || appTags?.length === 0 || appHasAlertTag === undefined) { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: App [${reloadParams.appId}] "${reloadParams.appName}" does not have the tag "${alertTag}" set. Not sending alert email based on app tag.`, + ); + } else if (appHasAlertTag !== undefined) { + // Add global send list from YAML config file to main send list + globalSendList.push(...emailConfig.globalSendList); + } + } + + // Get app owner info + const appOwner = await getQlikSenseCloudUserInfo(reloadParams.ownerId); + + // Get recipients based on app owner settings + // Build separate list of app owner email addresses + if (emailConfig.appOwnerAlert.enable === true) { + let appOwnerSendList = []; + + if (appOwner.email === undefined || appOwner?.email?.length === 0) { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: App owner email address is not set for app [${reloadParams.appId}] "${reloadParams.appName}". Not sending alert email to app owner "${appOwner.name}".`, + ); + } else { + // App owner email address exists. + + // Should *all* app owners get alerts? + if (emailConfig.appOwnerAlert.includeOwner.includeAll === true) { + // Add app owner's email address to app owner send list. + // Only do this if the email address length is greater than 0 + appOwnerSendList.push(appOwner.email); + } else { + // Check if app owner's email address is in the include list + const appOwnerIncludeList = emailConfig.appOwnerAlert.includeOwner?.user; + if (appOwnerIncludeList !== undefined && appOwnerIncludeList.length > 0) { + const appOwnerIsIncluded = appOwnerIncludeList.find((owner) => owner.email === appOwner.email); + + if (appOwnerIsIncluded !== undefined) { + // Add app owner's email address to app owner send list + appOwnerSendList.push(appOwner.email); + } + } + } + + // Now evaluate the exclusion list + const appOwnerExcludeList = emailConfig.appOwnerAlert.excludeOwner?.user; + if (appOwnerExcludeList !== undefined && appOwnerExcludeList.length > 0) { + // Exclusion list found. + // Remove all entries in exclude list from app owner send list + appOwnerSendList = appOwnerSendList.filter((ownerEmail) => { + const appOwnerIsExcluded = appOwnerExcludeList.find((exclude) => exclude.email === ownerEmail); + if (appOwnerIsExcluded === undefined) { + return ownerEmail; + } + }); + } + + // Add app owner send list to main send list + globalSendList.push(...appOwnerSendList); + } + } + + // Remove any duplicate email addresses from the main send list + globalSendList = [...new Set(globalSendList)]; + + // Check if we have any email addresses to send to + if (globalSendList.length === 0) { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: No email addresses found to send alert email for app [${reloadParams.appId}] "${reloadParams.appName}".`, + ); + return false; + } + + if (isSmtpConfigOk() === false) { + return false; + } + + // Get script logs, if enabled in the config file + // If the value is false, the script log could not be obtained + let scriptLogData = {}; + + if (reloadParams.scriptLog === false) { + scriptLogData = { + scriptLogFull: [], + scriptLogSize: 0, + scriptLogHead: '', + scriptLogHeadCount: 0, + scriptLogTail: '', + scriptLogTailCount: 0, + }; + } else { + // Reduce script log lines to only the ones we want to send to email + scriptLogData.scriptLogHeadCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines', + ); + scriptLogData.scriptLogTailCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines', + ); + + if (reloadParams.scriptLog?.scriptLogFull?.length > 0) { + // Get length of script log (character count) + scriptLogData.scriptLogSize = reloadParams.scriptLog.scriptLogFull.length; + + // Get the first and last n lines of the script log + scriptLogData.scriptLogHead = reloadParams.scriptLog.scriptLogFull + .slice(0, reloadParams.scriptLog.scriptLogHeadCount) + .join('\r\n'); + + scriptLogData.scriptLogTail = reloadParams.scriptLog.scriptLogFull + .slice(Math.max(reloadParams.scriptLog.scriptLogFull.length - reloadParams.scriptLog.scriptLogTailCount, 0)) + .join('\r\n'); + } else { + scriptLogData.scriptLogHead = ''; + scriptLogData.scriptLogTail = ''; + scriptLogData.scriptLogSize = 0; + } + + globals.logger.debug( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Script log data:\n${JSON.stringify(scriptLogData, null, 2)}`, + ); + } + + // Format log message line breaks to work in HTML email + if (reloadParams.reloadInfo.log !== undefined) { + // Replace \n with \r\n + reloadParams.reloadInfo.log = reloadParams.reloadInfo.log.replace(/(\r\n|\n|\r)/gm, '\r\n'); + } + + // Get Sense URLs from config file. Can be used as template fields. + const senseUrls = getQlikSenseCloudUrls(); + + // These are the template fields that can be used in email body + const templateContext = { + tenantId: reloadParams.tenantId, + tenantComment: reloadParams.tenantComment, + tenantUrl: reloadParams.tenantUrl, + + userId: reloadParams.userId, + userName: appOwner === undefined ? 'Unknown' : appOwner.name, + + appId: reloadParams.appId, + appName: reloadParams.appName, + appDescription: reloadParams.appInfo.attributes.description, + appUrl: reloadParams.appUrl, + appHasSectionAccess: reloadParams.appInfo.attributes.hasSectionAccess, + appIsPublished: reloadParams.appInfo.attributes.published, + appPublishTime: reloadParams.appInfo.attributes.publishTime, + appThumbnail: reloadParams.appInfo.attributes.thumbnail, + + reloadTrigger: reloadParams.reloadTrigger, + source: reloadParams.source, + eventType: reloadParams.eventType, + eventTypeVersion: reloadParams.eventTypeVersion, + endedWithMemoryConstraint: reloadParams.endedWithMemoryConstraint, + isDirectQueryMode: reloadParams.isDirectQueryMode, + isPartialReload: reloadParams.isPartialReload, + isSessionApp: reloadParams.isSessionApp, + isSkipStore: reloadParams.isSkipStore, + + peakMemoryBytes: reloadParams.peakMemoryBytes.toLocaleString(), + reloadId: reloadParams.reloadId, + rowLimit: reloadParams.rowLimit.toLocaleString(), + statements: reloadParams.statements, + status: reloadParams.status, + usageDuration: reloadParams.duration, + sizeMemoryBytes: reloadParams.sizeMemory.toLocaleString(), + appFileSize: reloadParams.appItems.resourceSize.appFile.toLocaleString(), + + errorCode: reloadParams.reloadInfo.errorCode, + errorMessage: reloadParams.reloadInfo.errorMessage, + logMessage: reloadParams.reloadInfo.log, + executionDuration: reloadParams.reloadInfo.executionDuration, + executionStartTime: reloadParams.reloadInfo.executionStartTime, + executionStopTime: reloadParams.reloadInfo.executionStopTime, + executionStatusText: reloadParams.reloadInfo.status, + scriptLogSize: scriptLogData.scriptLogSize.toLocaleString(), + scriptLogHead: scriptLogData.scriptLogHead, + scriptLogTail: scriptLogData.scriptLogTail, + scriptLogTailCount: scriptLogData.scriptLogTailCount, + scriptLogHeadCount: scriptLogData.scriptLogHeadCount, + + qlikSenseQMC: senseUrls.qmcUrl, + qlikSenseHub: senseUrls.hubUrl, + + appOwnerName: appOwner.name, + appOwnerUserId: appOwner.id, + appOwnerPicture: appOwner.picture, + appOwnerEmail: appOwner.email, + }; + + // Send alert emails + // Take into account rate limiting, basing it on appId + email address + for (const recipientEmailAddress of globalSendList) { + rateLimiterMemoryFailedReloads + .consume(`${reloadParams.taskId}|${recipientEmailAddress}`, 1) + // eslint-disable-next-line no-loop-func + .then(async (rateLimiterRes) => { + try { + globals.logger.info( + `EMAIL ALERT - QS CLOUD: Rate limiting check passed for failed app reload notification. App name: "${reloadParams.appName}", email: "${recipientEmailAddress}"`, + ); + globals.logger.debug( + `EMAIL ALERT - QS CLOUD: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, + ); + + // Only send email if there is an actual email address + if (recipientEmailAddress.length > 0) { + sendEmail( + emailConfig.fromAddress, + [recipientEmailAddress], + emailConfig.priority, + emailConfig.subject, + emailConfig.bodyFileDirectory, + emailConfig.htmlTemplateFile, + templateContext, + ); + } else { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: No email address found for app [${reloadParams.appId}] "${reloadParams.appName}". Not sending alert email.`, + ); + } + } catch (err) { + globals.logger.error(`EMAIL ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + } + }) + .catch((err) => { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification email for app [${reloadParams.appId}] "${reloadParams.appName}"`, + ); + globals.logger.debug( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(err, null, 2)}"`, + ); + }); + } + } catch (err) { + globals.logger.error(`EMAIL ALERT - QS CLOUD APP RELOAD FAILED: ${err}`); + } + return true; + }) + .catch((rateLimiterRes) => { + globals.logger.warn( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting failed. Not sending reload notification email for app [${reloadParams.appId}] "${reloadParams.appName}"`, + ); + globals.logger.debug( + `EMAIL ALERT - QS CLOUD APP RELOAD FAILED: Rate limiting details "${JSON.stringify(rateLimiterRes, null, 2)}"`, + ); + }); +} diff --git a/src/lib/qscloud/mqtt_event_app_reload_finished.js b/src/lib/qscloud/mqtt_event_app_reload_finished.js index a82bb8e4..4418198c 100644 --- a/src/lib/qscloud/mqtt_event_app_reload_finished.js +++ b/src/lib/qscloud/mqtt_event_app_reload_finished.js @@ -1,13 +1,15 @@ -/* eslint-disable import/prefer-default-export */ import globals from '../../globals.js'; import { getQlikSenseCloudAppReloadScriptLog, getQlikSenseCloudAppReloadInfo } from './api/appreloadinfo.js'; -import { getQlikSenseCloudAppInfo } from './api/app.js'; +import { getQlikSenseCloudAppInfo, getQlikSenseCloudAppMetadata, getQlikSenseCloudAppItems } from './api/app.js'; import { sendQlikSenseCloudAppReloadFailureNotificationTeams } from './msteams_notification_qscloud.js'; import { sendQlikSenseCloudAppReloadFailureNotificationSlack } from './slack_notification_qscloud.js'; +import { sendQlikSenseCloudAppReloadFailureNotificationEmail } from './email_notification_qscloud.js'; const { config, logger } = globals; // Function to handle Qlik Sense Cloud app reload finished event +// These events are received as MQTT messages, via a gateway that forwards the Qlik Sense Cloud webhook API events to MQTT +// // Parameters: // - message: MQTT message object, as sent by Qlik Sense Cloud webhook API export async function handleQlikSenseCloudAppReloadFinished(message) { @@ -69,6 +71,8 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { let scriptLog = {}; let reloadInfo = {}; let appInfo = {}; + let appMetadata = {}; + let appItems = {}; // App reload did fail. Send enabled notifications/alerts logger.info(`QLIK SENSE CLOUD: App reload failed. App ID=[${appId}] name="${message.data.name}"`); @@ -159,13 +163,60 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { reloadInfo.reloadId = reloadId; } + // App metadata is available via "GET /v1/apps/{appId}/data/metadata" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-data-metadata + try { + appMetadata = await getQlikSenseCloudAppMetadata(appId); + + logger.verbose(`QLIK SENSE CLOUD: App metadata obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App metadata: ${JSON.stringify(appMetadata, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app metadata. Error=${JSON.stringify(err, null, 2)}`); + } + + // App items are available via "GET /v1/items" + // https://qlik.dev/apis/rest/items/#get-v1-items + try { + appItems = await getQlikSenseCloudAppItems(appId); + + // There should be exactly one item in the appItems.data array, with a resourceId property that is the same as the app ID + // error if not + if (appItems?.data.length !== 1 || appItems?.data[0].resourceId !== appId) { + logger.error( + `QLIK SENSE CLOUD: App items obtained, but app ID does not match. App ID="${appId}", appItems="${JSON.stringify( + appItems, + null, + 2, + )}"`, + ); + + // Set appItems to empty object + appItems = {}; + } + + logger.verbose(`QLIK SENSE CLOUD: App items obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App items: ${JSON.stringify(appItems, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app items. Error=${JSON.stringify(err, null, 2)}`); + } + + // Get info from config file + const tenantUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl'); + + // Build URL to the app in Qlik Sense Cloud + // Format: /sense/app/ + // Take into account that tenant URL might have a trailing slash + const appUrl = `${tenantUrl}${tenantUrl.endsWith('/') ? '' : '/'}sense/app/${appId}`; + sendQlikSenseCloudAppReloadFailureNotificationTeams({ tenantId, tenantComment, + tenantUrl, userId, ownerId, appId, appName, + appUrl, reloadTrigger, source, @@ -190,6 +241,8 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { scriptLog, reloadInfo, appInfo, + appMetadata, + appItems: appItems?.data[0], }); } @@ -277,14 +330,212 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { reloadInfo.reloadId = reloadId; } + // App metadata is available via "GET /v1/apps/{appId}/data/metadata" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-data-metadata + try { + appMetadata = await getQlikSenseCloudAppMetadata(appId); + + logger.verbose(`QLIK SENSE CLOUD: App metadata obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App metadata: ${JSON.stringify(appMetadata, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app metadata. Error=${JSON.stringify(err, null, 2)}`); + } + + // App items are available via "GET /v1/items" + // https://qlik.dev/apis/rest/items/#get-v1-items + try { + appItems = await getQlikSenseCloudAppItems(appId); + + // There should be exactly one item in the appItems.data array, with a resourceId property that is the same as the app ID + // error if not + if (appItems?.data.length !== 1 || appItems?.data[0].resourceId !== appId) { + logger.error( + `QLIK SENSE CLOUD: App items obtained, but app ID does not match. App ID="${appId}", appItems="${JSON.stringify( + appItems, + null, + 2, + )}"`, + ); + + // Set appItems to empty object + appItems = {}; + } + + logger.verbose(`QLIK SENSE CLOUD: App items obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App items: ${JSON.stringify(appItems, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app items. Error=${JSON.stringify(err, null, 2)}`); + } + + // Get info from config file + const tenantUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl'); + + // Build URL to the app in Qlik Sense Cloud + // Format: /sense/app/ + // Take into account that tenant URL might have a trailing slash + const appUrl = `${tenantUrl}${tenantUrl.endsWith('/') ? '' : '/'}sense/app/${appId}`; + // Send Slack notification sendQlikSenseCloudAppReloadFailureNotificationSlack({ tenantId, tenantComment, + tenantUrl, + userId, + ownerId, + appId, + appName, + appUrl, + reloadTrigger, + + source, + eventType, + eventTypeVersion, + duration, + endedWithMemoryConstraint, + errors, + isDirectQueryMode, + isPartialReload, + isSessionApp, + isSkipStore, + peakMemoryBytes, + reloadId, + rowLimit, + statements, + status, + usage, + warnings, + sizeMemory, + + scriptLog, + reloadInfo, + appInfo, + appMetadata, + appItems: appItems?.data[0], + }); + } + + // Send email when an app reload has failed, if enabled + if ( + globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable') && + globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable') === + true + ) { + logger.verbose(`QLIK SENSE CLOUD: Sending email notification about app reload failure`); + + // Get extended info about the event + // This includes: + // - Reload script log + // - Reload info + // - App info + // - App metadata + // - App items + + // Script log is available via "GET /v1/apps/{appId}/reloads/logs/{reloadId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-reloads-logs-reloadId + try { + const headLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines', + ); + + const tailLineCount = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines', + ); + + scriptLog = await getQlikSenseCloudAppReloadScriptLog(appId, reloadId, headLineCount, tailLineCount); + + // If return value is false, the script log could not be obtained + if (scriptLog === false) { + logger.warn( + `QLIK SENSE CLOUD: Could not get app reload script log. App ID="${appId}", reload ID="${reloadId}"`, + ); + } else { + logger.verbose( + `QLIK SENSE CLOUD: App reload script log obtained. App ID="${appId}", reload ID="${reloadId}"`, + ); + } + logger.debug(`QLIK SENSE CLOUD: App reload script log: ${scriptLog}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app reload script log. Error=${JSON.stringify(err, null, 2)}`); + } + + // Reload info is available via "GET /v1/reloads/{reloadId}" + // https://qlik.dev/apis/rest/reloads/#get-v1-reloads-reloadId + try { + reloadInfo = await getQlikSenseCloudAppReloadInfo(reloadId); + reloadTrigger = reloadInfo.type; + + logger.verbose(`QLIK SENSE CLOUD: App reload info obtained. App ID="${appId}", reload ID="${reloadId}"`); + logger.debug(`QLIK SENSE CLOUD: App reload info: ${JSON.stringify(reloadInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app reload info. Error=${JSON.stringify(err, null, 2)}`); + } + + // App info is available via "GET /v1/apps/{appId}" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId + try { + appInfo = await getQlikSenseCloudAppInfo(appId); + + logger.verbose(`QLIK SENSE CLOUD: App info obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App info: ${JSON.stringify(appInfo, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app info. Error=${JSON.stringify(err, null, 2)}`); + } + + // App metadata is available via "GET /v1/apps/{appId}/data/metadata" + // https://qlik.dev/apis/rest/apps/#get-v1-apps-appId-data-metadata + try { + appMetadata = await getQlikSenseCloudAppMetadata(appId); + + logger.verbose(`QLIK SENSE CLOUD: App metadata obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App metadata: ${JSON.stringify(appMetadata, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app metadata. Error=${JSON.stringify(err, null, 2)}`); + } + + // App items are available via "GET /v1/items" + // https://qlik.dev/apis/rest/items/#get-v1-items + try { + appItems = await getQlikSenseCloudAppItems(appId); + + // There should be exactly one item in the appItems.data array, with a resourceId property that is the same as the app ID + // error if not + if (appItems?.data.length !== 1 || appItems?.data[0].resourceId !== appId) { + logger.error( + `QLIK SENSE CLOUD: App items obtained, but app ID does not match. App ID="${appId}", appItems="${JSON.stringify( + appItems, + null, + 2, + )}"`, + ); + + // Set appItems to empty object + appItems = {}; + } + + logger.verbose(`QLIK SENSE CLOUD: App items obtained. App ID="${appId}"`); + logger.debug(`QLIK SENSE CLOUD: App items: ${JSON.stringify(appItems, null, 2)}`); + } catch (err) { + logger.error(`QLIK SENSE CLOUD: Could not get app items. Error=${JSON.stringify(err, null, 2)}`); + } + + // Get info from config file + const tenantUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl'); + + // Build URL to the app in Qlik Sense Cloud + // Format: /sense/app/ + // Take into account that tenant URL might have a trailing slash + const appUrl = `${tenantUrl}${tenantUrl.endsWith('/') ? '' : '/'}sense/app/${appId}`; + + // Send email notification + sendQlikSenseCloudAppReloadFailureNotificationEmail({ + tenantId, + tenantComment, + tenantUrl, userId, ownerId, appId, appName, + appUrl, reloadTrigger, source, @@ -309,6 +560,8 @@ export async function handleQlikSenseCloudAppReloadFinished(message) { scriptLog, reloadInfo, appInfo, + appMetadata, + appItems: appItems?.data[0], }); } } diff --git a/src/lib/qscloud/msteams_notification_qscloud.js b/src/lib/qscloud/msteams_notification_qscloud.js index fc76af4a..65468bd5 100644 --- a/src/lib/qscloud/msteams_notification_qscloud.js +++ b/src/lib/qscloud/msteams_notification_qscloud.js @@ -8,11 +8,11 @@ import { RateLimiterMemory } from 'rate-limiter-flexible'; import globals from '../../globals.js'; import { getQlikSenseCloudUserInfo } from './api/user.js'; import { getQlikSenseCloudAppInfo } from './api/app.js'; -// import getAppOwner from '../../qrs_util/get_app_owner.js'; +import { getQlikSenseCloudUrls } from './util.js'; let rateLimiterMemoryFailedReloads; -if (globals.config.has('Butler.teamsNotification.reloadTaskFailure.rateLimit')) { +if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit')) { rateLimiterMemoryFailedReloads = new RateLimiterMemory({ points: 1, duration: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit'), @@ -73,24 +73,6 @@ function getAppReloadFailedTeamsConfig() { } } -function getQlikSenseCloudUrls() { - let qmcUrl = ''; - let hubUrl = ''; - - if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { - qmcUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc'); - } - - if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { - hubUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub'); - } - - return { - qmcUrl, - hubUrl, - }; -} - async function sendTeams(teamsWebhookUrl, teamsConfig, templateContext, msgType) { try { let compiledTemplate; @@ -262,10 +244,19 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams const templateContext = { tenantId: reloadParams.tenantId, tenantComment: reloadParams.tenantComment, + tenantUrl: reloadParams.tenantUrl, + userId: reloadParams.userId, userName: appOwner === undefined ? 'Unknown' : appOwner.name, + appId: reloadParams.appId, appName: reloadParams.appName, + appDescription: reloadParams.appInfo.attributes.description, + appUrl: reloadParams.appUrl, + appHasSectionAccess: reloadParams.appInfo.attributes.hasSectionAccess, + appIsPublished: reloadParams.appInfo.attributes.published, + appPublishTime: reloadParams.appInfo.attributes.publishTime, + appThumbnail: reloadParams.appInfo.attributes.thumbnail, reloadTrigger: reloadParams.reloadTrigger, source: reloadParams.source, @@ -276,13 +267,15 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams isPartialReload: reloadParams.isPartialReload, isSessionApp: reloadParams.isSessionApp, isSkipStore: reloadParams.isSkipStore, - peakMemoryBytes: reloadParams.peakMemoryBytes, + + peakMemoryBytes: reloadParams.peakMemoryBytes.toLocaleString(), reloadId: reloadParams.reloadId, - rowLimit: reloadParams.rowLimit, + rowLimit: reloadParams.rowLimit.toLocaleString(), statements: reloadParams.statements, status: reloadParams.status, usageDuration: reloadParams.duration, - sizeMemoryBytes: reloadParams.sizeMemory, + sizeMemoryBytes: reloadParams.sizeMemory.toLocaleString(), + appFileSize: reloadParams.appItems.resourceSize.appFile.toLocaleString(), errorCode: reloadParams.reloadInfo.errorCode, errorMessage: reloadParams.reloadInfo.errorMessage, @@ -294,7 +287,7 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams executionStartTime: reloadParams.reloadInfo.executionStartTime, executionStopTime: reloadParams.reloadInfo.executionStopTime, executionStatusText: reloadParams.reloadInfo.status, - scriptLogSize: scriptLogData.scriptLogSize, + scriptLogSize: scriptLogData.scriptLogSize.toLocaleString(), scriptLogHead: scriptLogData.scriptLogHead .replace(/([\r])/gm, '') .replace(/([\n])/gm, '\\n') @@ -305,8 +298,10 @@ export function sendQlikSenseCloudAppReloadFailureNotificationTeams(reloadParams .replace(/([\t])/gm, '\\t'), scriptLogTailCount: scriptLogData.scriptLogTailCount, scriptLogHeadCount: scriptLogData.scriptLogHeadCount, + qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.name, appOwnerUserId: appOwner.id, appOwnerPicture: appOwner.picture, diff --git a/src/lib/qscloud/slack_notification_qscloud.js b/src/lib/qscloud/slack_notification_qscloud.js index f5335144..58112a30 100644 --- a/src/lib/qscloud/slack_notification_qscloud.js +++ b/src/lib/qscloud/slack_notification_qscloud.js @@ -6,11 +6,11 @@ import globals from '../../globals.js'; import slackSend from '../slack_api.js'; import { getQlikSenseCloudUserInfo } from './api/user.js'; import { getQlikSenseCloudAppInfo } from './api/app.js'; -// import getAppOwner from '../../qrs_util/get_app_owner.js'; +import { getQlikSenseCloudUrls } from './util.js'; let rateLimiterMemoryFailedReloads; -if (globals.config.has('Butler.slackNotification.reloadTaskFailure.rateLimit')) { +if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit')) { rateLimiterMemoryFailedReloads = new RateLimiterMemory({ points: 1, duration: globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit'), @@ -94,24 +94,6 @@ function getAppReloadFailedSlackConfig() { } } -function getQlikSenseCloudUrls() { - let qmcUrl = ''; - let hubUrl = ''; - - if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { - qmcUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc'); - } - - if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { - hubUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub'); - } - - return { - qmcUrl, - hubUrl, - }; -} - async function sendSlack(slackConfig, templateContext, msgType) { try { let compiledTemplate; @@ -287,10 +269,19 @@ export function sendQlikSenseCloudAppReloadFailureNotificationSlack(reloadParams const templateContext = { tenantId: reloadParams.tenantId, tenantComment: reloadParams.tenantComment, + tenantUrl: reloadParams.tenantUrl, + userId: reloadParams.userId, userName: appOwner === undefined ? 'Unknown' : appOwner.name, + appId: reloadParams.appId, appName: reloadParams.appName, + appDescription: reloadParams.appInfo.attributes.description, + appUrl: reloadParams.appUrl, + appHasSectionAccess: reloadParams.appInfo.attributes.hasSectionAccess, + appIsPublished: reloadParams.appInfo.attributes.published, + appPublishTime: reloadParams.appInfo.attributes.publishTime, + appThumbnail: reloadParams.appInfo.attributes.thumbnail, reloadTrigger: reloadParams.reloadTrigger, source: reloadParams.source, @@ -301,13 +292,15 @@ export function sendQlikSenseCloudAppReloadFailureNotificationSlack(reloadParams isPartialReload: reloadParams.isPartialReload, isSessionApp: reloadParams.isSessionApp, isSkipStore: reloadParams.isSkipStore, - peakMemoryBytes: reloadParams.peakMemoryBytes, + + peakMemoryBytes: reloadParams.peakMemoryBytes.toLocaleString(), reloadId: reloadParams.reloadId, - rowLimit: reloadParams.rowLimit, + rowLimit: reloadParams.rowLimit.toLocaleString(), statements: reloadParams.statements, status: reloadParams.status, usageDuration: reloadParams.duration, - sizeMemoryBytes: reloadParams.sizeMemory, + sizeMemoryBytes: reloadParams.sizeMemory.toLocaleString(), + appFileSize: reloadParams.appItems.resourceSize.appFile.toLocaleString(), errorCode: reloadParams.reloadInfo.errorCode, errorMessage: reloadParams.reloadInfo.errorMessage, @@ -319,7 +312,7 @@ export function sendQlikSenseCloudAppReloadFailureNotificationSlack(reloadParams executionStartTime: reloadParams.reloadInfo.executionStartTime, executionStopTime: reloadParams.reloadInfo.executionStopTime, executionStatusText: reloadParams.reloadInfo.status, - scriptLogSize: scriptLogData.scriptLogSize, + scriptLogSize: scriptLogData.scriptLogSize.toLocaleString(), scriptLogHead: scriptLogData.scriptLogHead .replace(/([\r])/gm, '') .replace(/([\n])/gm, '\\n') @@ -330,8 +323,10 @@ export function sendQlikSenseCloudAppReloadFailureNotificationSlack(reloadParams .replace(/([\t])/gm, '\\t'), scriptLogTailCount: scriptLogData.scriptLogTailCount, scriptLogHeadCount: scriptLogData.scriptLogHeadCount, + qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.name, appOwnerUserId: appOwner.id, appOwnerPicture: appOwner.picture, diff --git a/src/lib/qscloud/util.js b/src/lib/qscloud/util.js new file mode 100644 index 00000000..616a4155 --- /dev/null +++ b/src/lib/qscloud/util.js @@ -0,0 +1,19 @@ +import globals from '../../globals.js'; + +export function getQlikSenseCloudUrls() { + let qmcUrl = ''; + let hubUrl = ''; + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { + qmcUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc'); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { + hubUrl = globals.config.get('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub'); + } + + return { + qmcUrl, + hubUrl, + }; +} diff --git a/src/lib/smtp.js b/src/lib/smtp.js index be1fcfbe..40ebedb0 100644 --- a/src/lib/smtp.js +++ b/src/lib/smtp.js @@ -41,7 +41,7 @@ if (globals.config.has('Butler.emailNotification.serviceStopped.rateLimit')) { rateLimiterMemoryServiceMonitor = new RateLimiterMemory({ points: 1, duration: 300 }); } -function isSmtpConfigOk() { +export function isSmtpConfigOk() { try { // First make sure email sending is enabled in the config file if ( @@ -224,7 +224,7 @@ function getQlikSenseUrls() { return { qmcUrl, hubUrl }; } -async function sendEmail(from, recipientsEmail, emailPriority, subjectHandlebars, viewPath, bodyFileHandlebars, templateContext) { +export async function sendEmail(from, recipientsEmail, emailPriority, subjectHandlebars, viewPath, bodyFileHandlebars, templateContext) { try { // First make sure email sending is enabled in the config file and that we have all required SMTP settings if (isSmtpConfigOk() === false) { From d1ebfaa50e668b3b2e945aff48ceb2fc476235e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Fri, 27 Sep 2024 14:33:02 +0000 Subject: [PATCH 05/14] refactor(config): Better verification of config file during startup Implements #1236 --- package-lock.json | 663 ++- package.json | 19 +- src/butler.js | 13 +- src/config/production_template.yaml | 9 + src/lib/assert/assert_config_file copy.js | 5437 +++++++++++++++++++++ src/lib/assert/assert_config_file.js | 5016 +------------------ src/lib/assert/config-file-schema.js | 3031 ++++++++++++ 7 files changed, 8832 insertions(+), 5356 deletions(-) create mode 100644 src/lib/assert/assert_config_file copy.js create mode 100755 src/lib/assert/config-file-schema.js diff --git a/package-lock.json b/package-lock.json index d68ad708..b844fa0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,14 +19,15 @@ "@fastify/swagger-ui": "^4.1.0", "@keyvhq/core": "^2.1.1", "@xstate/fsm": "^2.0.1", + "ajv": "^8.17.1", + "ajv-keywords": "^5.1.0", "any-base": "^1.1.0", - "axios": "^1.7.5", + "axios": "^1.7.7", "commander": "^12.1.0", "config": "^3.3.12", "cron-job-manager": "^2.3.1", "email-validator": "^2.0.4", "enigma.js": "^2.14.0", - "esbuild": "^0.23.1", "express-handlebars": "^7.1.3", "fastify": "^4.28.1", "fastify-healthcheck": "^4.4.0", @@ -39,15 +40,14 @@ "is-unc-path": "^1.0.0", "jjsontree.js": "^2.9.0", "js-yaml": "^4.1.0", - "jshint": "^2.13.6", "lodash": "^4.17.21", "luxon": "^3.5.0", "mkdirp": "^3.0.1", "moment": "^2.30.1", "moment-precise-range-plugin": "^1.3.0", - "mqtt": "^5.10.0", + "mqtt": "^5.10.1", "ms-teams-wrapper": "^1.0.2", - "nodemailer": "^6.9.14", + "nodemailer": "^6.9.15", "nodemailer-express-handlebars": "^6.1.2", "os": "^0.1.2", "posthog-node": "^4.2.0", @@ -61,17 +61,18 @@ "winston": "^3.14.2", "winston-daily-rotate-file": "^5.0.0", "ws": "^8.18.0", - "xstate": "^5.17.4" + "xstate": "^5.18.2" }, "devDependencies": { "@babel/eslint-parser": "^7.25.1", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@eslint/js": "^9.9.0", + "@babel/plugin-syntax-import-assertions": "^7.25.6", + "@eslint/js": "^9.11.1", + "esbuild": "^0.24.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "jest": "^29.7.0", "prettier": "^3.3.3", - "snyk": "^1.1292.4" + "snyk": "^1.1293.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -345,9 +346,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "license": "MIT", "engines": { @@ -553,13 +554,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -812,12 +813,14 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -827,12 +830,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -842,12 +847,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -857,12 +864,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -872,12 +881,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -887,12 +898,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -902,12 +915,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -917,12 +932,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -932,12 +949,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -947,12 +966,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -962,12 +983,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -977,12 +1000,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -992,12 +1017,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1007,12 +1034,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1022,12 +1051,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1037,12 +1068,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1052,12 +1085,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1067,12 +1102,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1082,12 +1119,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1097,12 +1136,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1112,12 +1153,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -1127,12 +1170,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1142,12 +1187,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1157,12 +1204,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1221,10 +1270,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@eslint/js": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", - "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", "dev": true, "license": "MIT", "engines": { @@ -1249,26 +1324,6 @@ "fast-uri": "^2.0.0" } }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/@fastify/autoload": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/@fastify/autoload/-/autoload-5.10.0.tgz", @@ -2383,16 +2438,15 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "peer": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2415,25 +2469,23 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "license": "MIT" }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -2547,9 +2599,10 @@ } }, "node_modules/axios": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2713,6 +2766,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2884,18 +2938,6 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, - "node_modules/cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha512-41U72MB56TfUMGndAKK8vJ78eooOD4Z5NOL4xEfjc0c23s+6EYKXlXsmACBVclLP1yOfWCgEganVzddVrSNoTg==", - "dependencies": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "engines": { - "node": ">=0.2.5" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3010,7 +3052,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/concat-stream": { "version": "2.0.0", @@ -3051,14 +3094,6 @@ "node": ">= 10.0.0" } }, - "node_modules/console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha512-duS7VP5pvfsNLDvL1O4VOEbw37AI3A4ZUQYemvDlnpGrNu9tprR7BYWpDYwC0Xia0Zxz5ZupdiIrUp0GH1aXfg==", - "dependencies": { - "date-now": "^0.1.4" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3084,11 +3119,6 @@ "node": ">= 0.6" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3139,11 +3169,6 @@ "node": ">= 8" } }, - "node_modules/date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha512-AsElvov3LoNB7tf5k37H2jYSB+ZZPMT5sG2QjJCcdlV5chIv6htBUBUui2IKRjgtKAKtCBN7Zbwa+MtwLjSeNw==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3264,56 +3289,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "node_modules/domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==", - "dependencies": { - "domelementtype": "1" - } - }, - "node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3368,11 +3343,6 @@ "resolved": "https://registry.npmjs.org/enigma.js/-/enigma.js-2.14.0.tgz", "integrity": "sha512-M84VjtO2w9+AUDK5NEn5j7FOPBdiSkwRbbgVFWxkLXxutH/z8ATGFh0Ko/4pTNwWF/9On/lEEr4BOI+ZewSw9g==" }, - "node_modules/entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==" - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3389,10 +3359,12 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -3400,30 +3372,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/escalade": { @@ -3592,6 +3564,32 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3710,6 +3708,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -3836,26 +3835,6 @@ "rfdc": "^1.2.0" } }, - "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -4038,13 +4017,14 @@ } }, "node_modules/find-my-way": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", - "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^2.0.0" + "safe-regex2": "^3.1.0" }, "engines": { "node": ">=14" @@ -4176,7 +4156,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -4259,6 +4240,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4480,39 +4462,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==", - "dependencies": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "node_modules/htmlparser2/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/htmlparser2/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/htmlparser2/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4623,6 +4572,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5497,45 +5447,6 @@ "node": ">=4" } }, - "node_modules/jshint": { - "version": "2.13.6", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.13.6.tgz", - "integrity": "sha512-IVdB4G0NTTeQZrBoM8C5JFVLjV2KtZ9APgybDA1MK73xb09qFs0jCXyQLnCOp1cSZZZbvhq/6mfXHUTaDkffuQ==", - "dependencies": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.21", - "minimatch": "~3.0.2", - "strip-json-comments": "1.0.x" - }, - "bin": { - "jshint": "bin/jshint" - } - }, - "node_modules/jshint/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jshint/node_modules/strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==", - "bin": { - "strip-json-comments": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5564,11 +5475,10 @@ } }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -5870,6 +5780,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5936,9 +5847,10 @@ } }, "node_modules/mqtt": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.0.tgz", - "integrity": "sha512-2qpkUi5Ftp8cBX4sPCh/yr4ULBfLFQkjlhTGVpilHznOlsmDWIligmT1anSaJ1FqiH29RONNZJXhcJQaFwddgQ==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.1.tgz", + "integrity": "sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==", + "license": "MIT", "dependencies": { "@types/readable-stream": "^4.0.5", "@types/ws": "^8.5.9", @@ -6021,9 +5933,9 @@ "dev": true }, "node_modules/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -6245,6 +6157,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -6729,11 +6642,12 @@ } }, "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/reusify": { @@ -6832,11 +6746,12 @@ ] }, "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", "dependencies": { - "ret": "~0.2.0" + "ret": "~0.4.0" } }, "node_modules/safe-stable-stringify": { @@ -6963,11 +6878,12 @@ } }, "node_modules/snyk": { - "version": "1.1292.4", - "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1292.4.tgz", - "integrity": "sha512-UwanRAL537nYVYEn7SwCOP2qkGl0QCGrSmzRlF7J73ffZUCK1p+xSR3GqB3zRqczXNh/fGLV8hTvupM6HrtTeg==", + "version": "1.1293.1", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1293.1.tgz", + "integrity": "sha512-CnbNrsEUMGfajfJ5/03BIgx1ixWKr9Kk+9xDw6sZqKy4K5K01DkyUp/V+WjbCfjr0li9+aE7u70s276KEOuiNA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@sentry/node": "^7.36.0", "global-agent": "^3.0.0" @@ -7702,9 +7618,10 @@ } }, "node_modules/xstate": { - "version": "5.17.4", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.17.4.tgz", - "integrity": "sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" diff --git a/package.json b/package.json index 083e68d2..992a840d 100644 --- a/package.json +++ b/package.json @@ -59,14 +59,15 @@ "@fastify/swagger-ui": "^4.1.0", "@keyvhq/core": "^2.1.1", "@xstate/fsm": "^2.0.1", + "ajv": "^8.17.1", + "ajv-keywords": "^5.1.0", "any-base": "^1.1.0", - "axios": "^1.7.5", + "axios": "^1.7.7", "commander": "^12.1.0", "config": "^3.3.12", "cron-job-manager": "^2.3.1", "email-validator": "^2.0.4", "enigma.js": "^2.14.0", - "esbuild": "^0.23.1", "express-handlebars": "^7.1.3", "fastify": "^4.28.1", "fastify-healthcheck": "^4.4.0", @@ -79,15 +80,14 @@ "is-unc-path": "^1.0.0", "jjsontree.js": "^2.9.0", "js-yaml": "^4.1.0", - "jshint": "^2.13.6", "lodash": "^4.17.21", "luxon": "^3.5.0", "mkdirp": "^3.0.1", "moment": "^2.30.1", "moment-precise-range-plugin": "^1.3.0", - "mqtt": "^5.10.0", + "mqtt": "^5.10.1", "ms-teams-wrapper": "^1.0.2", - "nodemailer": "^6.9.14", + "nodemailer": "^6.9.15", "nodemailer-express-handlebars": "^6.1.2", "os": "^0.1.2", "posthog-node": "^4.2.0", @@ -101,17 +101,18 @@ "winston": "^3.14.2", "winston-daily-rotate-file": "^5.0.0", "ws": "^8.18.0", - "xstate": "^5.17.4" + "xstate": "^5.18.2" }, "devDependencies": { "@babel/eslint-parser": "^7.25.1", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@eslint/js": "^9.9.0", + "@babel/plugin-syntax-import-assertions": "^7.25.6", + "@eslint/js": "^9.11.1", + "esbuild": "^0.24.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "jest": "^29.7.0", "prettier": "^3.3.3", - "snyk": "^1.1292.4" + "snyk": "^1.1293.1" }, "pkg": { "assets": [ diff --git a/src/butler.js b/src/butler.js index c3bd9780..0542c06c 100644 --- a/src/butler.js +++ b/src/butler.js @@ -40,22 +40,15 @@ const start = async () => { const udpInitTaskErrorServer = (await import('./udp/udp_handlers.js')).default; const mqttInitHandlers = (await import('./lib/mqtt_handlers.js')).default; - const { configFileStructureAssert, configFileYamlAssert, configFileNewRelicAssert, configFileInfluxDbAssert } = await import( + const { configFileStructureAssert, configFileNewRelicAssert, configFileInfluxDbAssert } = await import( './lib/assert/assert_config_file.js' ); - // Verify that config file is valid YAML - let resAssert = await configFileYamlAssert(globals.configFileExpanded); - if (resAssert === false) { - globals.logger.error('MAIN: Config file is not valid YAML. Exiting.'); - process.exit(1); - } else { - globals.logger.info('MAIN: Config file is valid YAML - all good.'); - } + let resAssert; // Verify correct structure of config file if (!settingsObj.options.skipConfigVerification) { - resAssert = await configFileStructureAssert(globals.config, globals.logger); + resAssert = await configFileStructureAssert(); if (resAssert === false) { globals.logger.error('MAIN: Config file structure is incorrect. Exiting.'); process.exit(1); diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 4cd2aed1..0442067b 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -954,6 +954,9 @@ Butler: teamsNotification: reloadAppFailure: enable: false + alertEnableByTag: + enable: false + tag: Butler - Send Teams alert if app reload fails basicContentOnly: false webhookURL: @@ -968,6 +971,9 @@ Butler: slackNotification: reloadAppFailure: enable: false + alertEnableByTag: + enable: false + tag: Butler - Send Slack alert if app reload fails basicContentOnly: false webhookURL: channel: sense-task-failure # Slack channel to which task failure notifications are sent @@ -985,6 +991,9 @@ Butler: emailNotification: reloadAppFailure: enable: false # Enable/disable app reload failed notifications via email + alertEnableByTag: + enable: false + tag: Butler - Send email if app reload fails appOwnerAlert: enable: false # Should app owner get notification email (assuming email address is available in Sense)? includeOwner: diff --git a/src/lib/assert/assert_config_file copy.js b/src/lib/assert/assert_config_file copy.js new file mode 100644 index 00000000..19d52b14 --- /dev/null +++ b/src/lib/assert/assert_config_file copy.js @@ -0,0 +1,5437 @@ +import QrsInteract from 'qrs-interact'; +import yaml from 'js-yaml'; +import { getReloadTasksCustomProperties } from '../../qrs_util/task_cp_util.js'; + +// Veriify InfluxDb related settings in the config file +export const configFileInfluxDbAssert = async (config, configQRS, logger) => { + // ------------------------------------------ + // The custom property specified by + // Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName + // should be present on reload tasks in the Qlik Sense server + + // Only test if the feature in question is enabled in the config file + if ( + config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable') === true && + config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName') && + config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue') + ) { + // Get custom property values + try { + const res1 = await getReloadTasksCustomProperties(config, configQRS, logger); + logger.debug(`ASSERT CONFIG INFLUXDB: The following custom properties are available for reload tasks: ${res1}`); + + // CEnsure that the CP name specified in the config file is found in the list of available CPs + // CP name is case sensitive and found in the "name" property of the CP object + if ( + res1.findIndex((cp) => cp.name === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName')) === + -1 + ) { + logger.error( + `ASSERT CONFIG INFLUXDB: Custom property '${config.get( + 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', + )}' not found in Qlik Sense. Aborting.`, + ); + return false; + } + + // Ensure that the CP value specified in the config file is found in the list of available CP values + // CP value is case sensitive and found in the "choiceValues" array of the CP objects in res1 + const res2 = res1.filter( + (cp) => cp.name === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName'), + )[0].choiceValues; + logger.debug( + `ASSERT CONFIG INFLUXDB: The following values are available for custom property '${config.get( + 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', + )}': ${res2}`, + ); + + if ( + res2.findIndex((cpValue) => cpValue === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue')) === + -1 + ) { + logger.error( + `ASSERT CONFIG INFLUXDB: Custom property value '${config.get( + 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue', + )}' not found for custom property '${config.get( + 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', + )}'. Aborting.`, + ); + return false; + } + } catch (err) { + logger.error(`ASSERT CONFIG INFLUXDB: ${err}`); + } + } + return true; +}; + +/** + * Verify New Relic settings in the config file + */ +export const configFileNewRelicAssert = async (config, configQRS, logger) => { + // Set up shared Sense repository service configuration + const cfg = { + hostname: config.get('Butler.configQRS.host'), + portNumber: 4242, + certificates: { + certFile: configQRS.certPaths.certPath, + keyFile: configQRS.certPaths.keyPath, + }, + }; + + cfg.headers = { + 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', + }; + + const qrsInstance = new QrsInteract(cfg); + + // ------------------------------------------ + // The custom property specified by + // Butler.incidentToo.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName + // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array + + // Only test if the feature in question is enabled in the config file + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName') + ) { + // Get custom property values + try { + logger.debug( + `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + logger.debug( + `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + + const result1 = await qrsInstance.Get( + `custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}'`, + ); + + // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic + + // If the custom property doesn't exist that's a problem.. + if (result1.body.length === 0) { + logger.error( + `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )})' does not exist in Qlik Sense. Aborting.`, + ); + return false; + } + + // If there are no choiceValues that's a problem.. + if ( + result1.body[0].choiceValues === undefined || + result1.body[0].choiceValues === null || + result1.body[0].choiceValues.length === 0 + ) { + logger.warn( + `ASSERT CONFIG NEW RELIC: Custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, + ); + } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { + // New Relic account specified as destination for events, but no account(s) specified in config file or on command line + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for alert events, but no New Relic account(s) specified on either command line or in config file. Aborting,`, + ); + return false; + } else { + // Test each custom property choice value for existence in Butler config file + const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); + + // eslint-disable-next-line no-restricted-syntax + for (const value of result1.body[0].choiceValues) { + if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}' not found in Butler's config file`, + ); + } + } + } + } catch (err) { + logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); + } + } else { + // eslint-disable-next-line no-lonely-if + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && + !config.has( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', + ) + ) { + logger.error( + `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName"`, + ); + return false; + } + } + + // ------------------------------------------ + // The custom property specified by + // Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName + // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array + + // Only test if the feature in question is enabled in the config file + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName') + ) { + // Get custom property values + try { + logger.debug( + `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + logger.debug( + `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + + const result1 = await qrsInstance.Get( + `custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}'`, + ); + // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic + + // If the custom property doesn't exist that's a problem.. + if (result1.body.length === 0) { + logger.error( + `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )})' does not exist in Qlik Sense. Aborting.`, + ); + return false; + } + + // If there are no choiceValues that's a problem.. + if ( + result1.body[0].choiceValues === undefined || + result1.body[0].choiceValues === null || + result1.body[0].choiceValues.length === 0 + ) { + logger.warn( + `ASSERT CONFIG NEW RELIC: Custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, + ); + } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { + // New Relic account specified as destination for events, but no account(s) specified in config file or on command line + logger.error( + `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for failed reload alert logs, but no New Relic account(s) specified on either command line or in config file. Aborting,`, + ); + return false; + } else { + // Test each custom property choice value for existence in Butler config file + const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); + + // eslint-disable-next-line no-restricted-syntax + for (const value of result1.body[0].choiceValues) { + if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}' not found in Butler's config file`, + ); + } + } + } + } catch (err) { + logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); + } + } else { + // eslint-disable-next-line no-lonely-if + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && + !config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName') + ) { + logger.error( + `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName"`, + ); + return false; + } + } + + // ------------------------------------------ + // The custom property specified by + // Butler.incidentToo.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName + // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array + + // Only test if the feature in question is enabled in the config file + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName') + ) { + // Get custom property values + try { + logger.debug( + `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + logger.debug( + `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + + const result1 = await qrsInstance.Get( + `custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}'`, + ); + // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic + + // If the custom property doesn't exist that's a problem.. + if (result1.body.length === 0) { + logger.error( + `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )})' does not exist in Qlik Sense. Aborting.`, + ); + return false; + } + + // If there are no choiceValues that's a problem.. + if ( + result1.body[0].choiceValues === undefined || + result1.body[0].choiceValues === null || + result1.body[0].choiceValues.length === 0 + ) { + logger.warn( + `ASSERT CONFIG NEW RELIC: Custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, + ); + } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { + // New Relic account specified as destination for events, but no account(s) specified in config file or on command line + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for alert events, but no New Relic account(s) specified on either command line or in config file. Aborting,`, + ); + return false; + } else { + // Test each custom property choice value for existence in Butler config file + const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); + + // eslint-disable-next-line no-restricted-syntax + for (const value of result1.body[0].choiceValues) { + if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + )}' not found in Butler's config file`, + ); + } + } + } + } catch (err) { + logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); + } + } else { + // eslint-disable-next-line no-lonely-if + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && + !config.has( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', + ) + ) { + logger.error( + `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName"`, + ); + return false; + } + } + + // ------------------------------------------ + // The custom property specified by + // Butler.incidentToo.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName + // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array + + // Only test if the feature in question is enabled in the config file + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName') + ) { + // Get custom property values + try { + logger.debug( + `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + logger.debug( + `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}`, + ); + + const result1 = await qrsInstance.Get( + `custompropertydefinition/full?filter=name eq '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}'`, + ); + // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic + + // If the custom property doesn't exist that's a problem.. + if (result1.body.length === 0) { + logger.error( + `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )})' does not exist in Qlik Sense. Aborting.`, + ); + return false; + } + + // If there are no choiceValues that's a problem.. + if ( + result1.body[0].choiceValues === undefined || + result1.body[0].choiceValues === null || + result1.body[0].choiceValues.length === 0 + ) { + logger.warn( + `ASSERT CONFIG NEW RELIC: Custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, + ); + } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { + // New Relic account specified as destination for events, but no account(s) specified in config file or on command line + logger.error( + `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for aborted reload alert logs, but no New Relic account(s) specified on either command line or in config file. Aborting,`, + ); + return false; + } else { + // Test each custom property choice value for existence in Butler config file + const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); + + // eslint-disable-next-line no-restricted-syntax + for (const value of result1.body[0].choiceValues) { + if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { + logger.warn( + `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( + 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', + )}' not found in Butler's config file`, + ); + } + } + } + } catch (err) { + logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); + } + } else { + // eslint-disable-next-line no-lonely-if + if ( + config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && + config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && + !config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName') + ) { + logger.error( + `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName"`, + ); + return false; + } + } + + return true; +}; + +// Function to verify that config file is valid YAML +export const configFileYamlAssert = async (configFile) => { + try { + const data = await yaml.load(configFile); + } catch (err) { + console.error(`ASSERT CONFIG: Config file is not valid YAML: ${err}`); + return false; + } + + return true; +}; + +// Function to verify that config variable have same structure as production.yaml file +export const configFileStructureAssert = async (config, logger) => { + let configFileCorrect = true; + + if (!config.has('Butler.logLevel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.logLevel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.fileLogging')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileLogging"'); + configFileCorrect = false; + } + + if (!config.has('Butler.logDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.logDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.anonTelemetry')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.anonTelemetry"'); + configFileCorrect = false; + } + + // Config visualisation setttings + if (!config.has('Butler.configVisualisation.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configVisualisation.host')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.host"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configVisualisation.port')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.port"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configVisualisation.obfuscate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.obfuscate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.heartbeat.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.heartbeat.remoteURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.remoteURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.heartbeat.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.frequency"'); + configFileCorrect = false; + } + + if (!config.has('Butler.dockerHealthCheck.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.dockerHealthCheck.port')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.port"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.frequency"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.logLevel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.logLevel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeInInfluxdb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeInInfluxdb.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.destinationAccount')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.destinationAccount"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.url')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.url"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.header array are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.uptimeMonitor.storeNewRelic.header')) { + const headers = config.get('Butler.uptimeMonitor.storeNewRelic.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error(`ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.header"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + + if (config.has('Butler.uptimeMonitor.storeNewRelic.attribute.static')) { + const staticAttributes = config.get('Butler.uptimeMonitor.storeNewRelic.attribute.static'); + + if (staticAttributes) { + if (!Array.isArray(staticAttributes)) { + logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static" is not an array'); + configFileCorrect = false; + } else { + staticAttributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.static"'); + configFileCorrect = false; + } + + if (!config.has('Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.thirdPartyToolsCredentials.newRelic are objects with the following properties: + // { + // accountName: 'string', + // insertApiKey: 'string' + // accountId: 123456 + // } + if (config.has('Butler.thirdPartyToolsCredentials.newRelic')) { + const newRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); + + if (newRelicAccounts) { + if (!Array.isArray(newRelicAccounts)) { + logger.error('ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic" is not an array'); + configFileCorrect = false; + } else { + newRelicAccounts.forEach((account, index) => { + if (typeof account !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(account, 'accountName')) { + logger.error( + `ASSERT CONFIG: Missing "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof account.accountName !== 'string') { + logger.error( + `ASSERT CONFIG: "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(account, 'insertApiKey')) { + logger.error( + `ASSERT CONFIG: Missing "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof account.insertApiKey !== 'string') { + logger.error( + `ASSERT CONFIG: "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(account, 'accountId')) { + logger.error( + `ASSERT CONFIG: Missing "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof account.accountId !== 'number') { + logger.error( + `ASSERT CONFIG: "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a number`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.thirdPartyToolsCredentials.newRelic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.hostIP')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostIP"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.hostPort')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostPort"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.auth.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.auth.username')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.username"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.auth.password')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.password"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.dbName')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.dbName"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.instanceTag')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.instanceTag"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.retentionPolicy.name')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.name"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.retentionPolicy.duration')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.duration"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskFailure.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tailScriptLogLines"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.influxDb.reloadTaskFailure.tag.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.influxDb.reloadTaskFailure.tag.static')) { + const tagsStatic = config.get('Butler.influxDb.reloadTaskFailure.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static" is not an array'); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.static"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.influxDb.reloadTaskSuccess.tag.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.influxDb.reloadTaskSuccess.tag.static')) { + const tagsStatic = config.get('Butler.influxDb.reloadTaskSuccess.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static" is not an array'); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.static"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags"'); + configFileCorrect = false; + } + + if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags"'); + configFileCorrect = false; + } + + if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseUrls.qmc')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.qmc"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseUrls.hub')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.hub"'); + configFileCorrect = false; + } + + // Qlik Sense version monitoring + if (!config.has('Butler.qlikSenseVersion.versionMonitor.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseVersion.versionMonitor.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.frequency"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseVersion.versionMonitor.host')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.host"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static')) { + const tagsStatic = config.get('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static" is not an array'); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); + configFileCorrect = false; + } + + // Qlik Sense server license monitoring + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.frequency"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enabled"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static')) { + const tagsStatic = config.get('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static" is not an array', + ); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enabled"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enabled"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable"', + ); + configFileCorrect = false; + } + + // Qlik Sense access license monitoring + if (!config.has('Butler.qlikSenseLicense.licenseMonitor.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseMonitor.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.frequency"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static')) { + const tagsStatic = config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static" is not an array'); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); + configFileCorrect = false; + } + + // Release Qlik Sense licenses + if (!config.has('Butler.qlikSenseLicense.licenseRelease.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.enable"'); + configFileCorrect = false; + } + + // License release dry run + if (!config.has('Butler.qlikSenseLicense.licenseRelease.dryRun')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.dryRun"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.frequency"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.user are objects with the following properties: + // { + // userDir: 'string' + // userId: 'string', + // } + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.user')) { + const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); + + if (neverReleaseUsers) { + if (!Array.isArray(neverReleaseUsers)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user" is not an array'); + configFileCorrect = false; + } else { + neverReleaseUsers.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userId !== 'string') { + logger.error( + `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(user, 'userDir')) { + logger.error( + `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userDir !== 'string') { + logger.error( + `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.user"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.tag objects with the following properties: + // - 'string' + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag')) { + const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); + + if (neverReleaseTags) { + if (!Array.isArray(neverReleaseTags)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag" is not an array'); + configFileCorrect = false; + } else { + neverReleaseTags.forEach((tag, index) => { + if (typeof tag !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag"'); + configFileCorrect = false; + } + + // The custom properties specified in the Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty array + // should meet the following requirements: + // - Each array item should be an object with the following properties: + // { + // name: 'string', + // value: 'string' + // } + + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory objects with the following properties: + // - 'string' + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory')) { + const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); + + if (neverReleaseUserDirectories) { + if (!Array.isArray(neverReleaseUserDirectories)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory" is not an array'); + configFileCorrect = false; + } else { + neverReleaseUserDirectories.forEach((userDirectory, index) => { + if (typeof userDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive')) { + const inactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive'); + + if (inactive) { + if (typeof inactive !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(inactive.toLowerCase())) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" must be either "Yes" or "No"'); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked')) { + const blocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked'); + + if (blocked) { + if (typeof blocked !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(blocked.toLowerCase())) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" must be either "Yes" or "No"'); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')) { + const removedExternally = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally'); + + if (removedExternally) { + if (typeof removedExternally !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(removedExternally.toLowerCase())) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" must be either "Yes" or "No"', + ); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable"'); + configFileCorrect = false; + } + + if (config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static')) { + const tagsStatic = config.get('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static'); + + if (tagsStatic) { + if (!Array.isArray(tagsStatic)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static" is not an array'); + configFileCorrect = false; + } else { + tagsStatic.forEach((tag, index) => { + if (typeof tag !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof tag.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static"'); + configFileCorrect = false; + } + + // Teams notifications + if (!config.has('Butler.teamsNotification.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskFailure.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStopped.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStopped.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStopped.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStopped.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStopped.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStarted.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStarted.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStarted.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStarted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.teamsNotification.serviceStarted.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.restMessage.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.restMessage.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.channel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.channel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.fromUser')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.fromUser"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskFailure.iconEmoji')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.iconEmoji"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.channel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.channel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.fromUser')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.fromUser"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.reloadTaskAborted.iconEmoji')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.iconEmoji"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.channel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.channel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.fromUser')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.fromUser"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStopped.iconEmoji')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.iconEmoji"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.webhookURL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.webhookURL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.channel')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.channel"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.messageType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.messageType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.basicMsgTemplate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.basicMsgTemplate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.templateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.templateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.fromUser')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.fromUser"'); + configFileCorrect = false; + } + + if (!config.has('Butler.slackNotification.serviceStarted.iconEmoji')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.iconEmoji"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user are objects with the following properties: + // { + // directory: 'string', + // userId: 'string' + // } + if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user')) { + const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user'); + + if (users) { + if (!Array.isArray(users)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user" is not an array'); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.directory !== 'string') { + logger.error( + `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userId !== 'string') { + logger.error( + `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user are objects with the following properties: + // { + // directory: 'string', + // userId: 'string' + // } + if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user')) { + const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user'); + + if (users) { + if (!Array.isArray(users)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user" is not an array'); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.directory !== 'string') { + logger.error( + `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userId !== 'string') { + logger.error( + `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.priority')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.priority"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.subject')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.subject"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.bodyFileDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.htmlTemplateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.htmlTemplateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskAborted.fromAdress')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.fromAdress"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskAborted.recipients are objects with the following properties: + // - 'string' + if (config.has('Butler.emailNotification.reloadTaskAborted.recipients')) { + const recipients = config.get('Butler.emailNotification.reloadTaskAborted.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients" is not an array'); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.recipients"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user are objects with the following properties: + // { + // directory: 'string', + // userId: 'string' + // } + if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user')) { + const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user'); + + if (users) { + if (!Array.isArray(users)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user" is not an array'); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.directory !== 'string') { + logger.error( + `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userId !== 'string') { + logger.error( + `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user are objects with the following properties: + // { + // directory: 'string', + // userId: 'string' + // } + if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user')) { + const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user'); + + if (users) { + if (!Array.isArray(users)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user" is not an array'); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { + logger.error( + `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.directory !== 'string') { + logger.error( + `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { + logger.error( + `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof user.userId !== 'string') { + logger.error( + `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.headScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.headScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.tailScriptLogLines')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.tailScriptLogLines"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.priority')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.priority"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.subject')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.subject"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.bodyFileDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.htmlTemplateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.htmlTemplateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.reloadTaskFailure.fromAdress')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.fromAdress"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.reloadTaskFailure.recipients are objects with the following properties: + // - 'string' + if (config.has('Butler.emailNotification.reloadTaskFailure.recipients')) { + const recipients = config.get('Butler.emailNotification.reloadTaskFailure.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients" is not an array'); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.recipients"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.priority')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.priority"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.subject')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.subject"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.bodyFileDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.bodyFileDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.htmlTemplateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.htmlTemplateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStopped.fromAdress')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.fromAdress"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.serviceStopped.recipients are objects with the following properties: + // - 'string' + if (config.has('Butler.emailNotification.serviceStopped.recipients')) { + const recipients = config.get('Butler.emailNotification.serviceStopped.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients" is not an array'); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.recipients"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.priority')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.priority"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.subject')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.subject"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.bodyFileDirectory')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.bodyFileDirectory"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.htmlTemplateFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.htmlTemplateFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.serviceStarted.fromAdress')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.fromAdress"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.emailNotification.serviceStarted.recipients are objects with the following properties: + // - 'string' + if (config.has('Butler.emailNotification.serviceStarted.recipients')) { + const recipients = config.get('Butler.emailNotification.serviceStarted.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients" is not an array'); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.recipients"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.host')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.host"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.port')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.port"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.secure')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.secure"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.tls.serverName')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.serverName"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.tls.ignoreTLS')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.ignoreTLS"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.tls.requireTLS')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.requireTLS"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.tls.rejectUnauthorized')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.rejectUnauthorized"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.auth.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.auth.user')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.user"'); + configFileCorrect = false; + } + + if (!config.has('Butler.emailNotification.smtp.auth.password')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.password"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.url')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.url"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.serviceName')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.serviceName"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.severity')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.severity"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId')) { + const appIds = config.get('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId'); + + if (appIds) { + if (!Array.isArray(appIds)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId" is not an array'); + configFileCorrect = false; + } else { + appIds.forEach((appId, index) => { + if (typeof appId !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.serviceName')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.serviceName"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.severity')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.severity"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.enable"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.event are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.destinationAccount.event')) { + const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.event'); + + if (destinationAccount) { + if (!Array.isArray(destinationAccount)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event" is not an array'); + configFileCorrect = false; + } else { + destinationAccount.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.event"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.log are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.destinationAccount.log')) { + const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.log'); + + if (destinationAccount) { + if (!Array.isArray(destinationAccount)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log" is not an array'); + configFileCorrect = false; + } else { + destinationAccount.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.log"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.url.event')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.url.event"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account')) { + const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account" is not an array', + ); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account')) { + const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account" is not an array', + ); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header')) { + const headers = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account')) { + const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account" is not an array', + ); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account')) { + const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account" is not an array', + ); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header')) { + const headers = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount')) { + const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount" is not an array', + ); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount are objects with the following properties: + // - 'string' + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount')) { + const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount'); + + if (accounts) { + if (!Array.isArray(accounts)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount" is not an array'); + configFileCorrect = false; + } else { + accounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header')) { + const headers = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static')) { + const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static"', + ); + configFileCorrect = false; + } + + // Butler.webhookNotification + if (!config.has('Butler.webhookNotification.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.enable"'); + configFileCorrect = false; + } + + // Butler.webhookNotification.reloadTaskFailure + if (!config.has('Butler.webhookNotification.reloadTaskFailure.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.webhookNotification.reloadTaskFailure.webhooks array are objects with the following properties: + // { + // "description": "A description of the webhook", + // "webhookURL": "https://webhook.site/...", + // "httpMethod": "POST", + // "cert": { + // "enable": true, + // "rejectUnauthorized": true, + // "certCA": "/path/to/ca-cert.pem", + // }, + // } + if (config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { + const webhooks = config.get('Butler.webhookNotification.reloadTaskFailure.webhooks'); + + if (webhooks) { + if (!Array.isArray(webhooks)) { + logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks" must be an array'); + configFileCorrect = false; + } else { + webhooks.forEach((webhook, index) => { + if (typeof webhook !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]" must be an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { + logger.error( + `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { + logger.error( + `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { + logger.error( + `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { + logger.error( + `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof webhook.cert !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { + logger.error( + `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { + logger.error( + `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { + logger.error( + `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); + configFileCorrect = false; + } + + if (!config.has('Butler.webhookNotification.reloadTaskAborted.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.webhookNotification.reloadTaskAborted.webhooks are objects with the following properties: + // { + // "description": "A description of the webhook", + // "webhookURL": "https://webhook.site/...", + // "httpMethod": "POST", + // "cert": { + // "enable": true, + // "rejectUnauthorized": true, + // "certCA": "/path/to/ca-cert.pem", + // }, + // } + if (config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { + const webhooks = config.get('Butler.webhookNotification.reloadTaskAborted.webhooks'); + // If there is one or more entries in Butler.webhookNotification.reloadTaskAborted.webhooks, verify that they are objects with the correct properties + if (webhooks) { + if (!Array.isArray(webhooks)) { + logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks" must be an array'); + configFileCorrect = false; + } else { + webhooks.forEach((webhook, index) => { + if (typeof webhook !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]" must be an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { + logger.error( + `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { + logger.error( + `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { + logger.error( + `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { + logger.error( + `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof webhook.cert !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { + logger.error( + `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { + logger.error( + `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { + logger.error( + `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); + configFileCorrect = false; + } + + if (!config.has('Butler.webhookNotification.serviceMonitor.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.rateLimit"'); + configFileCorrect = false; + } + + if (!config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.webhookNotification.serviceMonitor.webhooks are objects with the following properties: + // { + // "description": "A description of the webhook", + // "webhookURL": "https://webhook.site/...", + // "httpMethod": "POST", + // "cert": { + // "enable": true, + // "rejectUnauthorized": true, + // "certCA": "/path/to/ca-cert.pem", + // }, + // } + if (config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { + const webhooks = config.get('Butler.webhookNotification.serviceMonitor.webhooks'); + + if (webhooks) { + if (!Array.isArray(webhooks)) { + logger.error('ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks" must be an array'); + configFileCorrect = false; + } else { + webhooks.forEach((webhook, index) => { + if (typeof webhook !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}]" must be an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { + logger.error( + `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { + logger.error( + `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { + logger.error( + `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { + logger.error( + `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof webhook.cert !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { + logger.error( + `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { + logger.error( + `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { + logger.error( + `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); + configFileCorrect = false; + } + + // Butler.webhookNotification.qlikSenseServerLicenseMonitor + if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks are objects with the following properties: + // { + // "description": "A description of the webhook", + // "webhookURL": "https://webhook.site/...", + // "httpMethod": "POST", + // "cert": { + // "enable": true, + // "rejectUnauthorized": true, + // "certCA": "/path/to/ca-cert.pem", + // }, + // } + if (config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks')) { + const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks'); + + if (webhooks) { + if (!Array.isArray(webhooks)) { + logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks" must be an array'); + configFileCorrect = false; + } else { + webhooks.forEach((webhook, index) => { + if (typeof webhook !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { + logger.error( + `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { + logger.error( + `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { + logger.error( + `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { + logger.error( + `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof webhook.cert !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { + logger.error( + `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { + logger.error( + `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { + logger.error( + `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks"'); + configFileCorrect = false; + } + + // Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert + if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks are objects with the following properties: + // { + // "description": "A description of the webhook", + // "webhookURL": "https://webhook.site/...", + // "httpMethod": "POST", + // "cert": { + // "enable": true, + // "rejectUnauthorized": true, + // "certCA": "/path/to/ca-cert.pem", + // }, + // } + if (config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks')) { + const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks'); + + if (webhooks) { + if (!Array.isArray(webhooks)) { + logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks" must be an array'); + configFileCorrect = false; + } else { + webhooks.forEach((webhook, index) => { + if (typeof webhook !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { + logger.error( + `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { + logger.error( + `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { + logger.error( + `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { + logger.error( + `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof webhook.cert !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { + logger.error( + `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { + logger.error( + `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { + logger.error( + `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, + ); + configFileCorrect = false; + } + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks"'); + configFileCorrect = false; + } + + // Butler.scheduler + if (!config.has('Butler.scheduler.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.scheduler.configfile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.configfile"'); + configFileCorrect = false; + } + + // Butler.mqttConfig + if (!config.has('Butler.mqttConfig.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.brokerHost')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerHost"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.brokerPort')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerPort"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.azureEventGrid.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.azureEventGrid.clientId')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientId"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.azureEventGrid.clientCertFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientCertFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.azureEventGrid.clientKeyFile')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientKeyFile"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskFailureSendFull')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureSendFull"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskAbortedSendFull')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedSendFull"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.subscriptionRootTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.subscriptionRootTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskStartTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskStartTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskFailureTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskFailureFullTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureFullTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskAbortedTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.taskAbortedFullTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedFullTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.serviceRunningTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceRunningTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.serviceStoppedTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStoppedTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.serviceStatusTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStatusTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseTopic"'); + configFileCorrect = false; + } + + if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseExpireTopic')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseExpireTopic"'); + configFileCorrect = false; + } + + // QS Cloud settings + if (!config.has('Butler.qlikSenseCloud.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.id')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.id"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.authType')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.authType"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.comment')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.comment"'); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"', + ); + configFileCorrect = false; + } + + if ( + !config.has( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll', + ) + ) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll"', + ); + configFileCorrect = false; + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties: + // - email: 'string' + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) { + const users = config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user', + ); + + if (users) { + if (!Array.isArray(users)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user" must be an array', + ); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'email')) { + logger.error( + `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + } + }); + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user"', + ); + configFileCorrect = false; + } + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties: + // - email: 'string' + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) { + const users = config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user', + ); + + if (users) { + if (!Array.isArray(users)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user" must be an array', + ); + configFileCorrect = false; + } else { + users.forEach((user, index) => { + if (typeof user !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]" must be an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(user, 'email')) { + logger.error( + `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, + ); + configFileCorrect = false; + } + } + }); + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user"', + ); + configFileCorrect = false; + } + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress')) { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress"', + ); + configFileCorrect = false; + } + + // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings + // It is ok for the array to be empty + if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) { + const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients'); + + if (recipients) { + if (!Array.isArray(recipients)) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" must be an array', + ); + configFileCorrect = false; + } else { + recipients.forEach((recipient, index) => { + if (typeof recipient !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[${index}]" must be a string`, + ); + configFileCorrect = false; + } + }); + } + } else if (recipients === null) { + logger.warn( + 'ASSERT CONFIG: No recipients defined for Qlik Sense cloud alert emails, "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" is empty.', + ); + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', + ); + configFileCorrect = false; + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', + ); + configFileCorrect = false; + } + + // Butler.udpServerConfig + if (!config.has('Butler.udpServerConfig.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.udpServerConfig.serverHost')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.serverHost"'); + configFileCorrect = false; + } + + if (!config.has('Butler.udpServerConfig.portTaskFailure')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.portTaskFailure"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerConfig.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerConfig.serverHost')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverHost"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerConfig.serverPort')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverPort"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerConfig.backgroundServerPort')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.backgroundServerPort"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.fileCopyApprovedDirectories are objects with the following properties: + // { + // fromDirectory: 'string', + // toDirectory: 'string' + // } + if (config.has('Butler.fileCopyApprovedDirectories')) { + const directories = config.get('Butler.fileCopyApprovedDirectories'); + + if (directories) { + if (!Array.isArray(directories)) { + logger.error('ASSERT CONFIG: "Butler.fileCopyApprovedDirectories" is not an array'); + configFileCorrect = false; + } else { + directories.forEach((directory, index) => { + if (typeof directory !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.fileCopyApprovedDirectories[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { + logger.error( + `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof directory.fromDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { + logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`); + configFileCorrect = false; + } else if (typeof directory.toDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileCopyApprovedDirectories"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.fileMoveApprovedDirectories are objects with the following properties: + // { + // fromDirectory: 'string', + // toDirectory: 'string' + // } + if (config.has('Butler.fileMoveApprovedDirectories')) { + const directories = config.get('Butler.fileMoveApprovedDirectories'); + + if (directories) { + if (!Array.isArray(directories)) { + logger.error('ASSERT CONFIG: "Butler.fileMoveApprovedDirectories" is not an array'); + configFileCorrect = false; + } else { + directories.forEach((directory, index) => { + if (typeof directory !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.fileMoveApprovedDirectories[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { + logger.error( + `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof directory.fromDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { + logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`); + configFileCorrect = false; + } else if (typeof directory.toDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileMoveApprovedDirectories"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.fileDeleteApprovedDirectories are objects with the following properties: + // - name: 'string' + if (config.has('Butler.fileDeleteApprovedDirectories')) { + const directories = config.get('Butler.fileDeleteApprovedDirectories'); + + if (directories) { + if (!Array.isArray(directories)) { + logger.error('ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories" is not an array'); + configFileCorrect = false; + } else { + directories.forEach((directory, index) => { + if (typeof directory !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileDeleteApprovedDirectories"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerApiDocGenerate')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerApiDocGenerate"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.apiListEnbledEndpoints')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.apiListEnbledEndpoints"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.base62ToBase16')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base62ToBase16"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.base16ToBase62')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base16ToBase62"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.butlerping')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.butlerping"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.createDir')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDir"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.createDirQVD')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDirQVD"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.fileDelete')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileDelete"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.fileMove')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileMove"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.fileCopy')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileCopy"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.keyValueStore')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.keyValueStore"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.mqttPublishMessage')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.mqttPublishMessage"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.createNewSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.createNewSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.getSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.updateSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.updateSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.deleteSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.deleteSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.startSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.startSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.scheduler.stopSchedule')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.stopSchedule"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.senseAppReload')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppReload"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.senseAppDump')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppDump"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.senseListApps')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseListApps"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.senseStartTask')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseStartTask"'); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsEnable.slackPostMessage')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.slackPostMessage"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount are objects with the following properties: + // - name: 'string' + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount')) { + const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount'); + + if (destinationAccounts) { + if (!Array.isArray(destinationAccounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount" is not an array', + ); + configFileCorrect = false; + } else { + destinationAccounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) { + const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) { + const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static"', + ); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount are objects with the following properties: + // - name: 'string' + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount')) { + const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount'); + + if (destinationAccounts) { + if (!Array.isArray(destinationAccounts)) { + logger.error( + 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount" is not an array', + ); + configFileCorrect = false; + } else { + destinationAccounts.forEach((account, index) => { + if (typeof account !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount[${index}]" is not a string`, + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header')) { + const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header'); + + if (headers) { + if (!Array.isArray(headers)) { + logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header" is not an array'); + configFileCorrect = false; + } else { + headers.forEach((header, index) => { + if (typeof header !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(header, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(header, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof header.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static')) { + const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static'); + + if (attributes) { + if (!Array.isArray(attributes)) { + logger.error( + 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static" is not an array', + ); + configFileCorrect = false; + } else { + attributes.forEach((attribute, index) => { + if (typeof attribute !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof attribute.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error( + 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static"', + ); + configFileCorrect = false; + } + + if (!config.has('Butler.startTaskFilter.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.enable"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.startTaskFilter.allowTask.taskId are objects with the following properties: + // - name: 'string' + if (config.has('Butler.startTaskFilter.allowTask.taskId')) { + const taskIds = config.get('Butler.startTaskFilter.allowTask.taskId'); + + if (taskIds) { + if (!Array.isArray(taskIds)) { + logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId" is not an array'); + configFileCorrect = false; + } else { + taskIds.forEach((taskId, index) => { + if (typeof taskId !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.taskId"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.startTaskFilter.allowTask.tag are objects with the following properties: + // - name: 'string' + if (config.has('Butler.startTaskFilter.allowTask.tag')) { + const tags = config.get('Butler.startTaskFilter.allowTask.tag'); + + if (tags) { + if (!Array.isArray(tags)) { + logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag" is not an array'); + configFileCorrect = false; + } else { + tags.forEach((tag, index) => { + if (typeof tag !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.tag"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.startTaskFilter.allowTask.customProperty are objects with the following properties: + // { + // name: 'string', + // value: 'string' + // } + if (config.has('Butler.startTaskFilter.allowTask.customProperty')) { + const customProperties = config.get('Butler.startTaskFilter.allowTask.customProperty'); + + if (customProperties) { + if (!Array.isArray(customProperties)) { + logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty" is not an array'); + configFileCorrect = false; + } else { + customProperties.forEach((customProperty, index) => { + if (typeof customProperty !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(customProperty, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof customProperty.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(customProperty, 'value')) { + logger.error( + `ASSERT CONFIG: Missing "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, + ); + configFileCorrect = false; + } else if (typeof customProperty.value !== 'string') { + logger.error( + `ASSERT CONFIG: "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.customProperty"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.frequency')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.frequency"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.serviceMonitor.monitor are objects with the following properties: + // { + // host: 'string', + // services: { + // name: 'string', + // friendlyName: 'string', + // } + // } + if (config.has('Butler.serviceMonitor.monitor')) { + const monitors = config.get('Butler.serviceMonitor.monitor'); + + if (monitors) { + if (!Array.isArray(monitors)) { + logger.error('ASSERT CONFIG: "Butler.serviceMonitor.monitor" is not an array'); + configFileCorrect = false; + } else { + monitors.forEach((monitor, index) => { + if (typeof monitor !== 'object') { + logger.error(`ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}]" is not an object`); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(monitor, 'host')) { + logger.error(`ASSERT CONFIG: Missing "host" property in "Butler.serviceMonitor.monitor[${index}]"`); + configFileCorrect = false; + } else if (typeof monitor.host !== 'string') { + logger.error(`ASSERT CONFIG: "host" property in "Butler.serviceMonitor.monitor[${index}]" is not a string`); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(monitor, 'services')) { + logger.error(`ASSERT CONFIG: Missing "services" property in "Butler.serviceMonitor.monitor[${index}]"`); + configFileCorrect = false; + } else if (!Array.isArray(monitor.services)) { + logger.error(`ASSERT CONFIG: "services" property in "Butler.serviceMonitor.monitor[${index}]" is not an array`); + configFileCorrect = false; + } else { + monitor.services.forEach((service, serviceIndex) => { + if (typeof service !== 'object') { + logger.error( + `ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not an object`, + ); + configFileCorrect = false; + } else { + if (!Object.prototype.hasOwnProperty.call(service, 'name')) { + logger.error( + `ASSERT CONFIG: Missing "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, + ); + configFileCorrect = false; + } else if (typeof service.name !== 'string') { + logger.error( + `ASSERT CONFIG: "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, + ); + configFileCorrect = false; + } + + if (!Object.prototype.hasOwnProperty.call(service, 'friendlyName')) { + logger.error( + `ASSERT CONFIG: Missing "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, + ); + configFileCorrect = false; + } else if (typeof service.friendlyName !== 'string') { + logger.error( + `ASSERT CONFIG: "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, + ); + configFileCorrect = false; + } + } + }); + } + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.monitor"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.influxDb.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.influxDb.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.newRelic.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.newRelic.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.email.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.email.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.mqtt.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.mqtt.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.teams.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.teams.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.slack.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.slack.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.serviceMonitor.alertDestination.webhook.enable')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.webhook.enable"'); + configFileCorrect = false; + } + + if (!config.has('Butler.cert.clientCert')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCert"'); + configFileCorrect = false; + } + + if (!config.has('Butler.cert.clientCertKey')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertKey"'); + configFileCorrect = false; + } + + if (!config.has('Butler.cert.clientCertCA')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertCA"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.engineVersion')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.engineVersion"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.host')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.host"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.port')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.port"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.useSSL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.useSSL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.headers.X-Qlik-User')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.headers.X-Qlik-User"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configEngine.rejectUnauthorized')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.rejectUnauthorized"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.authentication')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.authentication"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.host')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.host"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.port')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.port"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.useSSL')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.useSSL"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.headerKey')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerKey"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.headerValue')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerValue"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configQRS.rejectUnauthorized')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.rejectUnauthorized"'); + configFileCorrect = false; + } + + if (!config.has('Butler.configDirectories.qvdPath')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.configDirectories.qvdPath"'); + configFileCorrect = false; + } + + return configFileCorrect; +}; diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js index 19d52b14..dde0de9b 100644 --- a/src/lib/assert/assert_config_file.js +++ b/src/lib/assert/assert_config_file.js @@ -1,10 +1,16 @@ import QrsInteract from 'qrs-interact'; -import yaml from 'js-yaml'; +import { load } from 'js-yaml'; +import fs from 'fs/promises'; +import { default as Ajv } from 'ajv'; + import { getReloadTasksCustomProperties } from '../../qrs_util/task_cp_util.js'; +import { confifgFileSchema } from './config-file-schema.js'; +import globals from '../../globals.js'; // Veriify InfluxDb related settings in the config file export const configFileInfluxDbAssert = async (config, configQRS, logger) => { // ------------------------------------------ + // The custom property specified by // Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName // should be present on reload tasks in the Qlik Sense server @@ -453,4985 +459,67 @@ export const configFileNewRelicAssert = async (config, configQRS, logger) => { return true; }; -// Function to verify that config file is valid YAML -export const configFileYamlAssert = async (configFile) => { +// Function to verify that config file has the correct format +export async function configFileStructureAssert() { try { - const data = await yaml.load(configFile); - } catch (err) { - console.error(`ASSERT CONFIG: Config file is not valid YAML: ${err}`); - return false; - } - - return true; -}; - -// Function to verify that config variable have same structure as production.yaml file -export const configFileStructureAssert = async (config, logger) => { - let configFileCorrect = true; - - if (!config.has('Butler.logLevel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.logLevel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.fileLogging')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileLogging"'); - configFileCorrect = false; - } - - if (!config.has('Butler.logDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.logDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.anonTelemetry')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.anonTelemetry"'); - configFileCorrect = false; - } - - // Config visualisation setttings - if (!config.has('Butler.configVisualisation.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.obfuscate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.obfuscate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.remoteURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.remoteURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.dockerHealthCheck.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.dockerHealthCheck.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.logLevel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.logLevel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeInInfluxdb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeInInfluxdb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.destinationAccount')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.destinationAccount"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.header array are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.uptimeMonitor.storeNewRelic.header')) { - const headers = config.get('Butler.uptimeMonitor.storeNewRelic.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error(`ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.header"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - - if (config.has('Butler.uptimeMonitor.storeNewRelic.attribute.static')) { - const staticAttributes = config.get('Butler.uptimeMonitor.storeNewRelic.attribute.static'); - - if (staticAttributes) { - if (!Array.isArray(staticAttributes)) { - logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static" is not an array'); - configFileCorrect = false; - } else { - staticAttributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.thirdPartyToolsCredentials.newRelic are objects with the following properties: - // { - // accountName: 'string', - // insertApiKey: 'string' - // accountId: 123456 - // } - if (config.has('Butler.thirdPartyToolsCredentials.newRelic')) { - const newRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - if (newRelicAccounts) { - if (!Array.isArray(newRelicAccounts)) { - logger.error('ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic" is not an array'); - configFileCorrect = false; - } else { - newRelicAccounts.forEach((account, index) => { - if (typeof account !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(account, 'accountName')) { - logger.error( - `ASSERT CONFIG: Missing "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.accountName !== 'string') { - logger.error( - `ASSERT CONFIG: "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(account, 'insertApiKey')) { - logger.error( - `ASSERT CONFIG: Missing "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.insertApiKey !== 'string') { - logger.error( - `ASSERT CONFIG: "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(account, 'accountId')) { - logger.error( - `ASSERT CONFIG: Missing "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.accountId !== 'number') { - logger.error( - `ASSERT CONFIG: "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a number`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.thirdPartyToolsCredentials.newRelic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.hostIP')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostIP"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.hostPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.username')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.username"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.password')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.password"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.dbName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.dbName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.instanceTag')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.instanceTag"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.retentionPolicy.name')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.name"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.retentionPolicy.duration')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.duration"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.influxDb.reloadTaskFailure.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.influxDb.reloadTaskFailure.tag.static')) { - const tagsStatic = config.get('Butler.influxDb.reloadTaskFailure.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.influxDb.reloadTaskSuccess.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.influxDb.reloadTaskSuccess.tag.static')) { - const tagsStatic = config.get('Butler.influxDb.reloadTaskSuccess.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseUrls.qmc')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.qmc"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseUrls.hub')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.hub"'); - configFileCorrect = false; - } - - // Qlik Sense version monitoring - if (!config.has('Butler.qlikSenseVersion.versionMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Qlik Sense server license monitoring - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enabled"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static" is not an array', - ); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static"', - ); - configFileCorrect = false; - } + const ajv = new Ajv({ + strict: true, + async: true, + allErrors: true, + }); - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enabled"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enabled"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable"', - ); - configFileCorrect = false; - } - - // Qlik Sense access license monitoring - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.enable"'); - configFileCorrect = false; - } + // Dynamically import ajv-keywords + const ajvKeywords = await import('ajv-keywords'); - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.frequency"'); - configFileCorrect = false; - } + // Add keywords to ajv instance + ajvKeywords.default(ajv); - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } + // Dynamically import ajv-formats + const ajvFormats = await import('ajv-formats'); - // Make sure all entries in Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static'); + // Add formats to ajv instance + ajvFormats.default(ajv); - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } + // Load the YAML schema file, identified by globals.configFile, from file + const fileContent = await fs.readFile(globals.configFileExpanded, 'utf8'); - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } + // Parse the YAML file + let parsedFileContent; + try { + parsedFileContent = load(fileContent); + } catch (err) { + throw new Error(`ASSERT CONFIG: Config file is not valid YAML: ${err}`); } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Release Qlik Sense licenses - if (!config.has('Butler.qlikSenseLicense.licenseRelease.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.enable"'); - configFileCorrect = false; - } - - // License release dry run - if (!config.has('Butler.qlikSenseLicense.licenseRelease.dryRun')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.dryRun"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.frequency"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.user are objects with the following properties: - // { - // userDir: 'string' - // userId: 'string', - // } - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.user')) { - const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); - - if (neverReleaseUsers) { - if (!Array.isArray(neverReleaseUsers)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user" is not an array'); - configFileCorrect = false; - } else { - neverReleaseUsers.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(user, 'userDir')) { - logger.error( - `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userDir !== 'string') { - logger.error( - `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.user"'); - configFileCorrect = false; - } + // Validate the parsed YAML file against the schema + const validate = ajv.compile(confifgFileSchema); + const valid = await validate(parsedFileContent); - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.tag objects with the following properties: - // - 'string' - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag')) { - const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); + if (!valid) { + // Log the errors in validate.errors[] and exit + // Each object in the error array has the following properties: + // - instancePath: Textual path to the part of the data that triggered the error + // - schemaPath: A JSON Pointer to the part of the schema that triggered the error + // - keyword: The validation keyword that failed + // - params: The parameters for the keyword + // - message: The error message - if (neverReleaseTags) { - if (!Array.isArray(neverReleaseTags)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag" is not an array'); - configFileCorrect = false; - } else { - neverReleaseTags.forEach((tag, index) => { - if (typeof tag !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag[${index}]" is not a string`); - configFileCorrect = false; - } - }); + for (const error of validate.errors) { + globals.logger.error(`VERIFY CONFIG FILE: ${error.instancePath} : ${error.message}`); } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag"'); - configFileCorrect = false; - } - - // The custom properties specified in the Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty array - // should meet the following requirements: - // - Each array item should be an object with the following properties: - // { - // name: 'string', - // value: 'string' - // } - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory objects with the following properties: - // - 'string' - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory')) { - const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); - - if (neverReleaseUserDirectories) { - if (!Array.isArray(neverReleaseUserDirectories)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory" is not an array'); - configFileCorrect = false; - } else { - neverReleaseUserDirectories.forEach((userDirectory, index) => { - if (typeof userDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } + process.exit(1); } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory"'); - configFileCorrect = false; - } - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive')) { - const inactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive'); + // ------------------------------ + // Verify values of specific config entries - if (inactive) { - if (typeof inactive !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(inactive.toLowerCase())) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" must be either "Yes" or "No"'); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive"'); - configFileCorrect = false; - } + globals.logger.info(`VERIFY CONFIG FILE: Your config file at ${globals.configFile} is valid, good work!`); - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked')) { - const blocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked'); + return true; + } catch (err) { + globals.logger.error(`VERIFY CONFIG FILE: ${err}`); - if (blocked) { - if (typeof blocked !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(blocked.toLowerCase())) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" must be either "Yes" or "No"'); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked"'); - configFileCorrect = false; + return false; } - - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')) { - const removedExternally = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally'); - - if (removedExternally) { - if (typeof removedExternally !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(removedExternally.toLowerCase())) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" must be either "Yes" or "No"', - ); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable"'); - configFileCorrect = false; - } - - if (config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static"'); - configFileCorrect = false; - } - - // Teams notifications - if (!config.has('Butler.teamsNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.restMessage.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.restMessage.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.reloadTaskAborted.recipients')) { - const recipients = config.get('Butler.emailNotification.reloadTaskAborted.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.reloadTaskFailure.recipients')) { - const recipients = config.get('Butler.emailNotification.reloadTaskFailure.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.serviceStopped.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.serviceStopped.recipients')) { - const recipients = config.get('Butler.emailNotification.serviceStopped.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.serviceStarted.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.serviceStarted.recipients')) { - const recipients = config.get('Butler.emailNotification.serviceStarted.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.secure')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.secure"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.serverName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.serverName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.ignoreTLS')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.ignoreTLS"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.requireTLS')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.requireTLS"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.user')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.user"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.password')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.password"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.url"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.serviceName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.serviceName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.severity')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.severity"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId')) { - const appIds = config.get('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId'); - - if (appIds) { - if (!Array.isArray(appIds)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId" is not an array'); - configFileCorrect = false; - } else { - appIds.forEach((appId, index) => { - if (typeof appId !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.serviceName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.serviceName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.severity')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.severity"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.event are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.destinationAccount.event')) { - const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.event'); - - if (destinationAccount) { - if (!Array.isArray(destinationAccount)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event" is not an array'); - configFileCorrect = false; - } else { - destinationAccount.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.event"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.log are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.destinationAccount.log')) { - const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.log'); - - if (destinationAccount) { - if (!Array.isArray(destinationAccount)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log" is not an array'); - configFileCorrect = false; - } else { - destinationAccount.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.log"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.url.event')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.url.event"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount')) { - const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount')) { - const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount" is not an array'); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - // Butler.webhookNotification - if (!config.has('Butler.webhookNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.enable"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.reloadTaskFailure - if (!config.has('Butler.webhookNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.reloadTaskFailure.webhooks array are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.reloadTaskFailure.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.reloadTaskAborted.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.reloadTaskAborted.webhooks'); - // If there is one or more entries in Butler.webhookNotification.reloadTaskAborted.webhooks, verify that they are objects with the correct properties - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.serviceMonitor.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.serviceMonitor.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.serviceMonitor.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.qlikSenseServerLicenseMonitor - if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert - if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks"'); - configFileCorrect = false; - } - - // Butler.scheduler - if (!config.has('Butler.scheduler.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scheduler.configfile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.configfile"'); - configFileCorrect = false; - } - - // Butler.mqttConfig - if (!config.has('Butler.mqttConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.brokerHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.brokerPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientId')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientId"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientCertFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientCertFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientKeyFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientKeyFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureSendFull')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureSendFull"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedSendFull')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedSendFull"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.subscriptionRootTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.subscriptionRootTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskStartTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskStartTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureFullTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureFullTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedFullTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedFullTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceRunningTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceRunningTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceStoppedTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStoppedTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceStatusTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStatusTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseExpireTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseExpireTopic"'); - configFileCorrect = false; - } - - // QS Cloud settings - if (!config.has('Butler.qlikSenseCloud.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.id')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.id"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.authType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.authType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.comment')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.comment"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"', - ); - configFileCorrect = false; - } - - if ( - !config.has( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll', - ) - ) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties: - // - email: 'string' - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) { - const users = config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user', - ); - - if (users) { - if (!Array.isArray(users)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user" must be an array', - ); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'email')) { - logger.error( - `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - } - }); - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties: - // - email: 'string' - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) { - const users = config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user', - ); - - if (users) { - if (!Array.isArray(users)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user" must be an array', - ); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'email')) { - logger.error( - `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - } - }); - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress"', - ); - configFileCorrect = false; - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings - // It is ok for the array to be empty - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) { - const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" must be an array', - ); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[${index}]" must be a string`, - ); - configFileCorrect = false; - } - }); - } - } else if (recipients === null) { - logger.warn( - 'ASSERT CONFIG: No recipients defined for Qlik Sense cloud alert emails, "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" is empty.', - ); - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', - ); - configFileCorrect = false; - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', - ); - configFileCorrect = false; - } - - // Butler.udpServerConfig - if (!config.has('Butler.udpServerConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.udpServerConfig.serverHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.serverHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.udpServerConfig.portTaskFailure')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.portTaskFailure"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.serverHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.serverPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.backgroundServerPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.backgroundServerPort"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileCopyApprovedDirectories are objects with the following properties: - // { - // fromDirectory: 'string', - // toDirectory: 'string' - // } - if (config.has('Butler.fileCopyApprovedDirectories')) { - const directories = config.get('Butler.fileCopyApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileCopyApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.fileCopyApprovedDirectories[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { - logger.error( - `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof directory.fromDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { - logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`); - configFileCorrect = false; - } else if (typeof directory.toDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileCopyApprovedDirectories"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileMoveApprovedDirectories are objects with the following properties: - // { - // fromDirectory: 'string', - // toDirectory: 'string' - // } - if (config.has('Butler.fileMoveApprovedDirectories')) { - const directories = config.get('Butler.fileMoveApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileMoveApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.fileMoveApprovedDirectories[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { - logger.error( - `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof directory.fromDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { - logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`); - configFileCorrect = false; - } else if (typeof directory.toDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileMoveApprovedDirectories"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileDeleteApprovedDirectories are objects with the following properties: - // - name: 'string' - if (config.has('Butler.fileDeleteApprovedDirectories')) { - const directories = config.get('Butler.fileDeleteApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileDeleteApprovedDirectories"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerApiDocGenerate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerApiDocGenerate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.apiListEnbledEndpoints')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.apiListEnbledEndpoints"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.base62ToBase16')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base62ToBase16"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.base16ToBase62')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base16ToBase62"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.butlerping')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.butlerping"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.createDir')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDir"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.createDirQVD')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDirQVD"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileDelete')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileDelete"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileMove')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileMove"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileCopy')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileCopy"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.keyValueStore')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.keyValueStore"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.mqttPublishMessage')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.mqttPublishMessage"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.createNewSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.createNewSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.getSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.updateSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.updateSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.deleteSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.deleteSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.startSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.startSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.stopSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.stopSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseAppReload')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppReload"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseAppDump')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppDump"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseListApps')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseListApps"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseStartTask')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseStartTask"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.slackPostMessage')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.slackPostMessage"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount are objects with the following properties: - // - name: 'string' - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount')) { - const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount'); - - if (destinationAccounts) { - if (!Array.isArray(destinationAccounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount" is not an array', - ); - configFileCorrect = false; - } else { - destinationAccounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) { - const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) { - const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount are objects with the following properties: - // - name: 'string' - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount')) { - const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount'); - - if (destinationAccounts) { - if (!Array.isArray(destinationAccounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount" is not an array', - ); - configFileCorrect = false; - } else { - destinationAccounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header')) { - const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static')) { - const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.startTaskFilter.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.taskId are objects with the following properties: - // - name: 'string' - if (config.has('Butler.startTaskFilter.allowTask.taskId')) { - const taskIds = config.get('Butler.startTaskFilter.allowTask.taskId'); - - if (taskIds) { - if (!Array.isArray(taskIds)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId" is not an array'); - configFileCorrect = false; - } else { - taskIds.forEach((taskId, index) => { - if (typeof taskId !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.taskId"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.tag are objects with the following properties: - // - name: 'string' - if (config.has('Butler.startTaskFilter.allowTask.tag')) { - const tags = config.get('Butler.startTaskFilter.allowTask.tag'); - - if (tags) { - if (!Array.isArray(tags)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag" is not an array'); - configFileCorrect = false; - } else { - tags.forEach((tag, index) => { - if (typeof tag !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.tag"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.customProperty are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.startTaskFilter.allowTask.customProperty')) { - const customProperties = config.get('Butler.startTaskFilter.allowTask.customProperty'); - - if (customProperties) { - if (!Array.isArray(customProperties)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty" is not an array'); - configFileCorrect = false; - } else { - customProperties.forEach((customProperty, index) => { - if (typeof customProperty !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(customProperty, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof customProperty.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(customProperty, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof customProperty.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.customProperty"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.frequency"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.serviceMonitor.monitor are objects with the following properties: - // { - // host: 'string', - // services: { - // name: 'string', - // friendlyName: 'string', - // } - // } - if (config.has('Butler.serviceMonitor.monitor')) { - const monitors = config.get('Butler.serviceMonitor.monitor'); - - if (monitors) { - if (!Array.isArray(monitors)) { - logger.error('ASSERT CONFIG: "Butler.serviceMonitor.monitor" is not an array'); - configFileCorrect = false; - } else { - monitors.forEach((monitor, index) => { - if (typeof monitor !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(monitor, 'host')) { - logger.error(`ASSERT CONFIG: Missing "host" property in "Butler.serviceMonitor.monitor[${index}]"`); - configFileCorrect = false; - } else if (typeof monitor.host !== 'string') { - logger.error(`ASSERT CONFIG: "host" property in "Butler.serviceMonitor.monitor[${index}]" is not a string`); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(monitor, 'services')) { - logger.error(`ASSERT CONFIG: Missing "services" property in "Butler.serviceMonitor.monitor[${index}]"`); - configFileCorrect = false; - } else if (!Array.isArray(monitor.services)) { - logger.error(`ASSERT CONFIG: "services" property in "Butler.serviceMonitor.monitor[${index}]" is not an array`); - configFileCorrect = false; - } else { - monitor.services.forEach((service, serviceIndex) => { - if (typeof service !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(service, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, - ); - configFileCorrect = false; - } else if (typeof service.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(service, 'friendlyName')) { - logger.error( - `ASSERT CONFIG: Missing "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, - ); - configFileCorrect = false; - } else if (typeof service.friendlyName !== 'string') { - logger.error( - `ASSERT CONFIG: "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.monitor"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.influxDb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.newRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.newRelic.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.email.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.email.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.mqtt.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.mqtt.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.teams.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.teams.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.slack.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.slack.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.webhook.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.webhook.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCert')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCert"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCertKey')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertKey"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCertCA')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertCA"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.engineVersion')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.engineVersion"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.useSSL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.useSSL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.headers.X-Qlik-User')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.headers.X-Qlik-User"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.authentication')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.authentication"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.useSSL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.useSSL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.headerKey')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerKey"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.headerValue')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerValue"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configDirectories.qvdPath')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configDirectories.qvdPath"'); - configFileCorrect = false; - } - - return configFileCorrect; -}; +} diff --git a/src/lib/assert/config-file-schema.js b/src/lib/assert/config-file-schema.js new file mode 100755 index 00000000..e08f80dd --- /dev/null +++ b/src/lib/assert/config-file-schema.js @@ -0,0 +1,3031 @@ +import { type } from 'os'; + +export const confifgFileSchema = { + type: 'object', + properties: { + Butler: { + type: 'object', + properties: { + logLevel: { + type: 'string', + enum: ['error', 'warn', 'info', 'verbose', 'debug', 'silly'], + transform: ['trim', 'toLowerCase'], + }, + fileLogging: { type: 'boolean' }, + logDirectory: { type: 'string' }, + anonTelemetry: { type: 'boolean' }, + configVisualisation: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + host: { + type: 'string', + format: 'hostname', + }, + port: { type: 'number' }, + obfuscate: { type: 'boolean' }, + }, + required: ['enable', 'host', 'port', 'obfuscate'], + additionalProperties: false, + }, + heartbeat: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + remoteURL: { + type: 'string', + format: 'uri', + }, + frequency: { type: 'string' }, + }, + required: ['enable', 'remoteURL', 'frequency'], + additionalProperties: false, + }, + dockerHealthCheck: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + port: { type: 'number' }, + }, + required: ['enable', 'port'], + additionalProperties: false, + }, + uptimeMonitor: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + frequency: { type: 'string' }, + logLevel: { + type: 'string', + enum: ['error', 'warn', 'info', 'verbose', 'debug', 'silly'], + transform: ['trim', 'toLowerCase'], + }, + storeInInfluxdb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + storeNewRelic: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + destinationAccount: { + type: ['array', 'null'], + minItems: 0, + items: { + type: 'string', + }, + }, + url: { + type: 'string', + format: 'uri', + }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + metric: { + type: 'object', + properties: { + dynamic: { + type: 'object', + properties: { + butlerMemoryUsage: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + butlerUptime: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['butlerMemoryUsage', 'butlerUptime'], + additionalProperties: false, + }, + }, + required: ['dynamic'], + additionalProperties: false, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + butlerVersion: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['butlerVersion'], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + }, + }, + required: ['enable', 'destinationAccount', 'url', 'header', 'metric', 'attribute'], + additionalProperties: false, + }, + }, + required: ['enable', 'frequency', 'logLevel', 'storeInInfluxdb', 'storeNewRelic'], + additionalProperties: false, + }, + thirdPartyToolsCredentials: { + type: 'object', + properties: { + newRelic: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + accountName: { type: 'string' }, + insertApiKey: { type: 'string' }, + accountId: { type: 'number' }, + }, + required: ['accountName', 'insertApiKey', 'accountId'], + additionalProperties: false, + }, + }, + }, + required: ['newRelic'], + additionalProperties: false, + }, + + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + hostIP: { + type: 'string', + format: 'hostname', + }, + hostPort: { type: 'number' }, + version: { type: 'number' }, + auth: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + username: { type: 'string' }, + password: { + type: 'string', + format: 'password', + }, + }, + required: ['enable', 'username', 'password'], + additionalProperties: false, + }, + dbName: { type: 'string' }, + instanceTag: { type: 'string' }, + retentionPolicy: { + type: 'object', + properties: { + name: { type: 'string' }, + duration: { type: 'string' }, + }, + required: ['name', 'duration'], + additionalProperties: false, + }, + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tailScriptLogLines: { type: 'number' }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'tailScriptLogLines', 'tag'], + additionalProperties: false, + }, + reloadTaskSuccess: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + allReloadTasks: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + byCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + enabledValue: { type: 'string' }, + }, + required: ['enable', 'customPropertyName', 'enabledValue'], + additionalProperties: false, + }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'allReloadTasks', 'byCustomProperty', 'tag'], + additionalProperties: false, + }, + }, + required: [ + 'enable', + 'hostIP', + 'hostPort', + 'auth', + 'dbName', + 'instanceTag', + 'retentionPolicy', + 'reloadTaskFailure', + 'reloadTaskSuccess', + ], + additionalProperties: false, + }, + + scriptLog: { + type: 'object', + properties: { + storeOnDisk: { + type: 'object', + properties: { + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + logDirectory: { type: 'string' }, + }, + required: ['enable', 'logDirectory'], + additionalProperties: false, + }, + }, + required: ['reloadTaskFailure'], + additionalProperties: false, + }, + }, + required: ['storeOnDisk'], + additionalProperties: false, + }, + + qlikSenseUrls: { + type: 'object', + properties: { + qmc: { + type: 'string', + format: 'uri', + }, + hub: { + type: 'string', + format: 'uri', + }, + }, + required: ['qmc', 'hub'], + additionalProperties: false, + }, + + qlikSenseVersion: { + type: 'object', + properties: { + versionMonitor: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + frequency: { type: 'string' }, + host: { + type: 'string', + format: 'hostname', + }, + rejectUnauthorized: { type: 'boolean' }, + destination: { + type: 'object', + properties: { + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + }, + required: ['influxDb'], + additionalProperties: false, + }, + }, + required: ['enable', 'frequency', 'host', 'rejectUnauthorized', 'destination'], + additionalProperties: false, + }, + }, + required: ['versionMonitor'], + additionalProperties: false, + }, + + qlikSenseLicense: { + type: 'object', + properties: { + serverLicenseMonitor: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + frequency: { type: 'string' }, + alert: { + type: 'object', + properties: { + thresholdDays: { type: 'number' }, + }, + required: ['thresholdDays'], + additionalProperties: false, + }, + destination: { + type: 'object', + properties: { + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + mqtt: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendRecurring: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + sendAlert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendRecurring', 'sendAlert'], + additionalProperties: false, + }, + webhook: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendRecurring: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + sendAlert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendRecurring', 'sendAlert'], + additionalProperties: false, + }, + }, + required: ['influxDb', 'mqtt', 'webhook'], + additionalProperties: false, + }, + }, + required: ['enable', 'frequency', 'alert', 'destination'], + additionalProperties: false, + }, + licenseMonitor: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + frequency: { type: 'string' }, + destination: { + type: 'object', + properties: { + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + }, + required: ['influxDb'], + additionalProperties: false, + }, + }, + required: ['enable', 'frequency', 'destination'], + additionalProperties: false, + }, + licenseRelease: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + dryRun: { type: 'boolean' }, + frequency: { type: 'string' }, + neverRelease: { + type: 'object', + properties: { + user: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + userDir: { type: 'string' }, + userId: { type: 'string' }, + }, + required: ['userDir', 'userId'], + additionalProperties: false, + }, + }, + tag: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + customProperty: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + userDirectory: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + inactive: { + type: 'string', + enum: ['yes', 'no', 'ignore'], + transform: ['trim', 'toLowerCase'], + }, + blocked: { + type: 'string', + enum: ['yes', 'no', 'ignore'], + transform: ['trim', 'toLowerCase'], + }, + removedExternally: { + type: 'string', + enum: ['yes', 'no', 'ignore'], + transform: ['trim', 'toLowerCase'], + }, + }, + required: [ + 'user', + 'tag', + 'customProperty', + 'userDirectory', + 'inactive', + 'blocked', + 'removedExternally', + ], + additionalProperties: false, + }, + licenseType: { + type: 'object', + properties: { + analyzer: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + releaseThresholdDays: { type: 'number' }, + }, + required: ['enable', 'releaseThresholdDays'], + additionalProperties: false, + }, + professional: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + releaseThresholdDays: { type: 'number' }, + }, + required: ['enable', 'releaseThresholdDays'], + additionalProperties: false, + }, + }, + required: ['analyzer', 'professional'], + additionalProperties: false, + }, + destination: { + type: 'object', + properties: { + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['enable', 'tag'], + }, + }, + required: ['influxDb'], + additionalProperties: false, + }, + }, + required: ['enable', 'dryRun', 'frequency', 'neverRelease', 'licenseType', 'destination'], + additionalProperties: false, + }, + }, + required: ['serverLicenseMonitor', 'licenseMonitor', 'licenseRelease'], + additionalProperties: false, + }, + + teamsNotification: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + }, + required: [ + 'enable', + 'webhookURL', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + ], + additionalProperties: false, + }, + reloadTaskAborted: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + }, + required: [ + 'enable', + 'webhookURL', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + ], + additionalProperties: false, + }, + serviceStopped: { + type: 'object', + properties: { + webhookURL: { + type: 'string', + format: 'uri', + }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + templateFile: { type: 'string' }, + }, + required: ['webhookURL', 'messageType', 'basicMsgTemplate', 'rateLimit', 'templateFile'], + additionalProperties: false, + }, + serviceStarted: { + type: 'object', + properties: { + webhookURL: { + type: 'string', + format: 'uri', + }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + templateFile: { type: 'string' }, + }, + required: ['webhookURL', 'messageType', 'basicMsgTemplate', 'rateLimit', 'templateFile'], + additionalProperties: false, + }, + }, + required: ['enable', 'reloadTaskFailure', 'reloadTaskAborted', 'serviceStopped', 'serviceStarted'], + additionalProperties: false, + }, + + slackNotification: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + restMessage: { + type: 'object', + properties: { + webhookURL: { + type: 'string', + format: 'uri', + }, + }, + required: ['webhookURL'], + additionalProperties: false, + }, + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + channel: { type: 'string' }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + fromUser: { type: 'string' }, + iconEmoji: { type: 'string' }, + }, + required: [ + 'enable', + 'webhookURL', + 'channel', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + 'fromUser', + 'iconEmoji', + ], + additionalProperties: false, + }, + reloadTaskAborted: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + channel: { type: 'string' }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + fromUser: { type: 'string' }, + iconEmoji: { type: 'string' }, + }, + required: [ + 'enable', + 'webhookURL', + 'channel', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + 'fromUser', + 'iconEmoji', + ], + additionalProperties: false, + }, + serviceStopped: { + type: 'object', + properties: { + webhookURL: { + type: 'string', + format: 'uri', + }, + channel: { type: 'string' }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + templateFile: { type: 'string' }, + fromUser: { type: 'string' }, + iconEmoji: { type: 'string' }, + }, + required: [ + 'webhookURL', + 'channel', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'templateFile', + 'fromUser', + 'iconEmoji', + ], + additionalProperties: false, + }, + serviceStarted: { + type: 'object', + properties: { + webhookURL: { + type: 'string', + format: 'uri', + }, + channel: { type: 'string' }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + templateFile: { type: 'string' }, + fromUser: { type: 'string' }, + iconEmoji: { type: 'string' }, + }, + required: [ + 'webhookURL', + 'channel', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'templateFile', + 'fromUser', + 'iconEmoji', + ], + additionalProperties: false, + }, + }, + required: ['enable', 'restMessage', 'reloadTaskFailure', 'reloadTaskAborted', 'serviceStopped', 'serviceStarted'], + additionalProperties: false, + }, + + emailNotification: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + reloadTaskAborted: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + appOwnerAlert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + includeOwner: { + type: 'object', + properties: { + includeAll: { type: 'boolean' }, + user: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + directory: { type: 'string' }, + userId: { type: 'string' }, + }, + required: ['directory', 'userId'], + additionalProperties: false, + }, + }, + }, + required: ['includeAll', 'user'], + additionalProperties: false, + }, + excludeOwner: { + type: 'object', + properties: { + user: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + directory: { type: 'string' }, + userId: { type: 'string' }, + }, + required: ['directory', 'userId'], + additionalProperties: false, + }, + }, + }, + required: ['user'], + additionalProperties: false, + }, + }, + required: ['enable', 'includeOwner', 'excludeOwner'], + additionalProperties: false, + }, + alertEnableByCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + enabledValue: { type: 'string' }, + }, + required: ['enable', 'customPropertyName', 'enabledValue'], + additionalProperties: false, + }, + alertEnabledByEmailAddress: { + type: 'object', + properties: { + customPropertyName: { type: 'string' }, + }, + required: ['customPropertyName'], + additionalProperties: false, + }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high'], + transform: ['trim', 'toLowerCase'], + }, + subject: { type: 'string' }, + bodyFileDirectory: { type: 'string' }, + htmlTemplateFile: { type: 'string' }, + fromAdress: { type: 'string' }, + recipients: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: [ + 'enable', + 'appOwnerAlert', + 'alertEnableByCustomProperty', + 'alertEnabledByEmailAddress', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'priority', + 'subject', + 'bodyFileDirectory', + 'htmlTemplateFile', + 'fromAdress', + 'recipients', + ], + additionalProperties: false, + }, + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + appOwnerAlert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + includeOwner: { + type: 'object', + properties: { + includeAll: { type: 'boolean' }, + user: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + directory: { type: 'string' }, + userId: { type: 'string' }, + }, + required: ['directory', 'userId'], + additionalProperties: false, + }, + }, + }, + required: ['includeAll', 'user'], + additionalProperties: false, + }, + excludeOwner: { + type: 'object', + properties: { + user: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + directory: { type: 'string' }, + userId: { type: 'string' }, + }, + required: ['directory', 'userId'], + additionalProperties: false, + }, + }, + }, + required: ['user'], + additionalProperties: false, + }, + }, + required: ['enable', 'includeOwner', 'excludeOwner'], + additionalProperties: false, + }, + alertEnableByCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + enabledValue: { type: 'string' }, + }, + required: ['enable', 'customPropertyName', 'enabledValue'], + additionalProperties: false, + }, + alertEnabledByEmailAddress: { + type: 'object', + properties: { + customPropertyName: { type: 'string' }, + }, + required: ['customPropertyName'], + additionalProperties: false, + }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high'], + transform: ['trim', 'toLowerCase'], + }, + subject: { type: 'string' }, + bodyFileDirectory: { type: 'string' }, + htmlTemplateFile: { type: 'string' }, + fromAdress: { type: 'string' }, + recipients: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: [ + 'enable', + 'appOwnerAlert', + 'alertEnableByCustomProperty', + 'alertEnabledByEmailAddress', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'priority', + 'subject', + 'bodyFileDirectory', + 'htmlTemplateFile', + 'fromAdress', + 'recipients', + ], + additionalProperties: false, + }, + serviceStopped: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high'], + transform: ['trim', 'toLowerCase'], + }, + subject: { type: 'string' }, + bodyFileDirectory: { type: 'string' }, + htmlTemplateFile: { type: 'string' }, + fromAdress: { type: 'string' }, + recipients: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: [ + 'rateLimit', + 'priority', + 'subject', + 'bodyFileDirectory', + 'htmlTemplateFile', + 'fromAdress', + 'recipients', + ], + additionalProperties: false, + }, + serviceStarted: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high'], + transform: ['trim', 'toLowerCase'], + }, + subject: { type: 'string' }, + bodyFileDirectory: { type: 'string' }, + htmlTemplateFile: { type: 'string' }, + fromAdress: { type: 'string' }, + recipients: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: [ + 'rateLimit', + 'priority', + 'subject', + 'bodyFileDirectory', + 'htmlTemplateFile', + 'fromAdress', + 'recipients', + ], + additionalProperties: false, + }, + smtp: { + type: 'object', + properties: { + host: { + type: 'string', + format: 'hostname', + }, + port: { type: 'number' }, + secure: { type: 'boolean' }, + tls: { + type: 'object', + properties: { + serverName: { type: ['string', 'null'] }, + ignoreTLS: { type: 'boolean' }, + requireTLS: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + }, + required: ['serverName', 'ignoreTLS', 'requireTLS', 'rejectUnauthorized'], + additionalProperties: false, + }, + auth: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + user: { type: 'string' }, + password: { + type: 'string', + format: 'password', + }, + }, + required: ['enable', 'user', 'password'], + additionalProperties: false, + }, + }, + required: ['host', 'port', 'secure', 'tls', 'auth'], + additionalProperties: false, + }, + }, + required: ['enable', 'reloadTaskAborted', 'reloadTaskFailure', 'serviceStopped', 'serviceStarted', 'smtp'], + additionalProperties: false, + }, + + incidentTool: { + type: 'object', + properties: { + signl4: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + url: { + type: 'string', + format: 'uri', + }, + reloadTaskFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rateLimit: { type: 'number' }, + serviceName: { type: 'string' }, + severity: { type: 'number' }, + includeApp: { + type: 'object', + properties: { + includeAll: { type: 'boolean' }, + appId: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['includeAll', 'appId'], + additionalProperties: false, + }, + }, + required: ['enable', 'rateLimit', 'serviceName', 'severity', 'includeApp'], + additionalProperties: false, + }, + reloadTaskAborted: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rateLimit: { type: 'number' }, + serviceName: { type: 'string' }, + severity: { type: 'number' }, + includeApp: { + type: 'object', + properties: { + includeAll: { type: 'boolean' }, + appId: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['includeAll', 'appId'], + additionalProperties: false, + }, + }, + required: ['enable', 'rateLimit', 'serviceName', 'severity', 'includeApp'], + additionalProperties: false, + }, + }, + required: ['enable', 'url', 'reloadTaskFailure', 'reloadTaskAborted'], + additionalProperties: false, + }, + newRelic: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + destinationAccount: { + type: 'object', + properties: { + event: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + log: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['event', 'log'], + additionalProperties: false, + }, + url: { + type: 'object', + properties: { + event: { + type: 'string', + format: 'uri', + }, + log: { + type: 'string', + format: 'uri', + }, + }, + required: ['event', 'log'], + additionalProperties: false, + }, + reloadTaskFailure: { + type: 'object', + properties: { + destination: { + type: 'object', + properties: { + event: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendToAccount: { + type: 'object', + properties: { + byCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + }, + required: ['enable', 'customPropertyName'], + additionalProperties: false, + }, + always: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + account: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['enable', 'account'], + additionalProperties: false, + }, + }, + required: ['byCustomProperty', 'always'], + additionalProperties: false, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + required: ['useAppTags', 'useTaskTags'], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + log: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tailScriptLogLines: { type: 'number' }, + sendToAccount: { + type: 'object', + properties: { + byCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + }, + required: ['enable', 'customPropertyName'], + additionalProperties: false, + }, + always: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + account: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['enable', 'account'], + additionalProperties: false, + }, + }, + required: ['byCustomProperty', 'always'], + additionalProperties: false, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'tailScriptLogLines', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + }, + required: ['event', 'log'], + additionalProperties: false, + }, + sharedSettings: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['rateLimit', 'header', 'attribute'], + additionalProperties: false, + }, + }, + required: ['destination', 'sharedSettings'], + additionalProperties: false, + }, + reloadTaskAborted: { + type: 'object', + properties: { + destination: { + type: 'object', + properties: { + event: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendToAccount: { + type: 'object', + properties: { + byCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + }, + required: ['enable', 'customPropertyName'], + additionalProperties: false, + }, + always: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + account: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['enable', 'account'], + additionalProperties: false, + }, + }, + required: ['byCustomProperty', 'always'], + additionalProperties: false, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + required: ['useAppTags', 'useTaskTags'], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + log: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tailScriptLogLines: { type: 'number' }, + sendToAccount: { + type: 'object', + properties: { + byCustomProperty: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + customPropertyName: { type: 'string' }, + }, + required: ['enable', 'customPropertyName'], + additionalProperties: false, + }, + always: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + account: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['enable', 'account'], + additionalProperties: false, + }, + }, + required: ['byCustomProperty', 'always'], + additionalProperties: false, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + useAppTags: { type: 'boolean' }, + useTaskTags: { type: 'boolean' }, + }, + required: ['useAppTags', 'useTaskTags'], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'tailScriptLogLines', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + }, + required: ['event', 'log'], + }, + sharedSettings: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['rateLimit', 'header', 'attribute'], + additionalProperties: false, + }, + }, + required: ['destination', 'sharedSettings'], + additionalProperties: false, + }, + serviceMonitor: { + type: 'object', + properties: { + destination: { + type: 'object', + properties: { + event: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendToAccount: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + serviceHost: { type: 'boolean' }, + serviceName: { type: 'boolean' }, + serviceDisplayName: { type: 'boolean' }, + serviceState: { type: 'boolean' }, + }, + required: [ + 'serviceHost', + 'serviceName', + 'serviceDisplayName', + 'serviceState', + ], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + log: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + sendToAccount: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + dynamic: { + type: 'object', + properties: { + serviceHost: { type: 'boolean' }, + serviceName: { type: 'boolean' }, + serviceDisplayName: { type: 'boolean' }, + serviceState: { type: 'boolean' }, + }, + required: [ + 'serviceHost', + 'serviceName', + 'serviceDisplayName', + 'serviceState', + ], + additionalProperties: false, + }, + }, + required: ['static', 'dynamic'], + additionalProperties: false, + }, + }, + required: ['enable', 'sendToAccount', 'attribute'], + additionalProperties: false, + }, + }, + required: ['event', 'log'], + additionalProperties: false, + }, + monitorServiceState: { + type: 'object', + properties: { + running: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + stopped: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['running', 'stopped'], + additionalProperties: false, + }, + sharedSettings: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['rateLimit', 'header', 'attribute'], + additionalProperties: false, + }, + }, + required: ['destination', 'monitorServiceState', 'sharedSettings'], + additionalProperties: false, + }, + }, + required: ['enable', 'destinationAccount', 'url', 'reloadTaskFailure', 'reloadTaskAborted', 'serviceMonitor'], + additionalProperties: false, + }, + }, + required: ['signl4', 'newRelic'], + additionalProperties: false, + }, + + webhookNotification: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + reloadTaskFailure: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + webhooks: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + description: { type: 'string' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + httpMethod: { + type: 'string', + enum: ['GET', 'POST', 'PUT'], + transform: ['trim', 'toUpperCase'], + }, + cert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + certCA: { type: 'string' }, + }, + required: ['enable', 'rejectUnauthorized', 'certCA'], + additionalProperties: false, + }, + }, + required: ['description', 'webhookURL', 'httpMethod', 'cert'], + }, + }, + }, + required: ['rateLimit', 'webhooks'], + additionalProperties: false, + }, + reloadTaskAborted: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + webhooks: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + description: { type: 'string' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + httpMethod: { + type: 'string', + enum: ['GET', 'POST', 'PUT'], + transform: ['trim', 'toUpperCase'], + }, + cert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + certCA: { type: 'string' }, + }, + required: ['enable', 'rejectUnauthorized', 'certCA'], + additionalProperties: false, + }, + }, + required: ['description', 'webhookURL', 'httpMethod', 'cert'], + }, + }, + }, + required: ['rateLimit', 'webhooks'], + additionalProperties: false, + }, + serviceMonitor: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + webhooks: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + description: { type: 'string' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + httpMethod: { + type: 'string', + enum: ['GET', 'POST', 'PUT'], + transform: ['trim', 'toUpperCase'], + }, + cert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + certCA: { type: 'string' }, + }, + required: ['enable', 'rejectUnauthorized', 'certCA'], + additionalProperties: false, + }, + }, + required: ['description', 'webhookURL', 'httpMethod', 'cert'], + }, + }, + }, + required: ['rateLimit', 'webhooks'], + additionalProperties: false, + }, + qlikSenseServerLicenseMonitor: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + webhooks: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + description: { type: 'string' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + httpMethod: { + type: 'string', + enum: ['GET', 'POST', 'PUT'], + transform: ['trim', 'toUpperCase'], + }, + cert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + certCA: { type: 'string' }, + }, + required: ['enable', 'rejectUnauthorized', 'certCA'], + additionalProperties: false, + }, + }, + required: ['description', 'webhookURL', 'httpMethod', 'cert'], + }, + }, + }, + required: ['rateLimit', 'webhooks'], + additionalProperties: false, + }, + qlikSenseServerLicenseExpiryAlert: { + type: 'object', + properties: { + rateLimit: { type: 'number' }, + webhooks: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + description: { type: 'string' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + httpMethod: { + type: 'string', + enum: ['GET', 'POST', 'PUT'], + transform: ['trim', 'toUpperCase'], + }, + cert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + rejectUnauthorized: { type: 'boolean' }, + certCA: { type: 'string' }, + }, + required: ['enable', 'rejectUnauthorized', 'certCA'], + additionalProperties: false, + }, + }, + required: ['description', 'webhookURL', 'httpMethod', 'cert'], + }, + }, + }, + required: ['rateLimit', 'webhooks'], + additionalProperties: false, + }, + }, + required: [ + 'enable', + 'reloadTaskFailure', + 'reloadTaskAborted', + 'serviceMonitor', + 'qlikSenseServerLicenseMonitor', + 'qlikSenseServerLicenseExpiryAlert', + ], + additionalProperties: false, + }, + + scheduler: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + configfile: { type: 'string' }, + }, + required: ['enable', 'configfile'], + additionalProperties: false, + }, + + keyValueStore: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + maxKeysPerNamespace: { type: 'number' }, + }, + required: ['enable', 'maxKeysPerNamespace'], + additionalProperties: false, + }, + + mqttConfig: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + brokerHost: { + type: 'string', + format: 'hostname', + }, + brokerPort: { type: 'number' }, + azureEventGrid: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + clientId: { type: 'string' }, + clientCertFile: { type: 'string' }, + clientKeyFile: { type: 'string' }, + }, + required: ['enable', 'clientId', 'clientCertFile', 'clientKeyFile'], + additionalProperties: false, + }, + taskFailureSendFull: { type: 'boolean' }, + taskAbortedSendFull: { type: 'boolean' }, + subscriptionRootTopic: { type: 'string' }, + taskStartTopic: { type: 'string' }, + taskFailureTopic: { type: 'string' }, + taskFailureFullTopic: { type: 'string' }, + taskFailureServerStatusTopic: { type: 'string' }, + taskAbortedTopic: { type: 'string' }, + taskAbortedFullTopic: { type: 'string' }, + serviceRunningTopic: { type: 'string' }, + serviceStoppedTopic: { type: 'string' }, + serviceStatusTopic: { type: 'string' }, + qlikSenseServerLicenseTopic: { type: 'string' }, + qlikSenseServerLicenseExpireTopic: { type: 'string' }, + qlikSenseCloud: { + type: 'object', + properties: { + event: { + type: 'object', + properties: { + mqttForward: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + broker: { + type: 'object', + properties: { + host: { + type: 'string', + format: 'hostname', + }, + port: { type: 'number' }, + username: { type: 'string' }, + password: { type: 'string' }, + }, + required: ['host', 'port', 'username', 'password'], + additionalProperties: false, + }, + topic: { + type: 'object', + properties: { + subscriptionRoot: { type: 'string' }, + appReload: { type: 'string' }, + }, + required: ['subscriptionRoot', 'appReload'], + additionalProperties: false, + }, + }, + required: ['enable', 'broker', 'topic'], + additionalProperties: false, + }, + }, + required: ['mqttForward'], + additionalProperties: false, + }, + }, + required: ['event'], + additionalProperties: false, + }, + }, + required: [ + 'enable', + 'brokerHost', + 'brokerPort', + 'azureEventGrid', + 'taskFailureSendFull', + 'taskAbortedSendFull', + 'subscriptionRootTopic', + 'taskStartTopic', + 'taskFailureTopic', + 'taskFailureFullTopic', + 'taskFailureServerStatusTopic', + 'taskAbortedTopic', + 'taskAbortedFullTopic', + 'serviceRunningTopic', + 'serviceStoppedTopic', + 'serviceStatusTopic', + 'qlikSenseServerLicenseTopic', + 'qlikSenseServerLicenseExpireTopic', + 'qlikSenseCloud', + ], + additionalProperties: false, + }, + + udpServerConfig: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + serverHost: { + type: 'string', + format: 'hostname', + }, + portTaskFailure: { type: 'number' }, + }, + required: ['enable', 'serverHost', 'portTaskFailure'], + additionalProperties: false, + }, + + restServerConfig: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + serverHost: { + type: 'string', + format: 'hostname', + }, + serverPort: { type: 'number' }, + backgroundServerPort: { type: 'number' }, + }, + required: ['enable', 'serverHost', 'serverPort', 'backgroundServerPort'], + additionalProperties: false, + }, + + fileCopyApprovedDirectories: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + fromDirectory: { type: 'string' }, + toDirectory: { type: 'string' }, + }, + required: ['fromDirectory', 'toDirectory'], + additionalProperties: false, + }, + }, + + fileMoveApprovedDirectories: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + fromDirectory: { type: 'string' }, + toDirectory: { type: 'string' }, + }, + required: ['fromDirectory', 'toDirectory'], + additionalProperties: false, + }, + }, + + fileDeleteApprovedDirectories: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + + restServerApiDocGenerate: { type: 'boolean' }, + + restServerEndpointsEnable: { + type: 'object', + properties: { + apiListEnbledEndpoints: { type: 'boolean' }, + base62ToBase16: { type: 'boolean' }, + base16ToBase62: { type: 'boolean' }, + butlerping: { type: 'boolean' }, + createDir: { type: 'boolean' }, + createDirQVD: { type: 'boolean' }, + fileDelete: { type: 'boolean' }, + fileMove: { type: 'boolean' }, + fileCopy: { type: 'boolean' }, + keyValueStore: { type: 'boolean' }, + mqttPublishMessage: { type: 'boolean' }, + newRelic: { + type: 'object', + properties: { + postNewRelicMetric: { type: 'boolean' }, + postNewRelicEvent: { type: 'boolean' }, + }, + required: ['postNewRelicMetric', 'postNewRelicEvent'], + additionalProperties: false, + }, + scheduler: { + type: 'object', + properties: { + createNewSchedule: { type: 'boolean' }, + getSchedule: { type: 'boolean' }, + getScheduleStatusAll: { type: 'boolean' }, + updateSchedule: { type: 'boolean' }, + deleteSchedule: { type: 'boolean' }, + startSchedule: { type: 'boolean' }, + stopSchedule: { type: 'boolean' }, + }, + required: [ + 'createNewSchedule', + 'getSchedule', + 'getScheduleStatusAll', + 'updateSchedule', + 'deleteSchedule', + 'startSchedule', + 'stopSchedule', + ], + additionalProperties: false, + }, + senseAppReload: { type: 'boolean' }, + senseAppDump: { type: 'boolean' }, + senseListApps: { type: 'boolean' }, + senseStartTask: { type: 'boolean' }, + slackPostMessage: { type: 'boolean' }, + }, + required: [ + 'apiListEnbledEndpoints', + 'base62ToBase16', + 'base16ToBase62', + 'butlerping', + 'createDir', + 'createDirQVD', + 'fileDelete', + 'fileMove', + 'fileCopy', + 'keyValueStore', + 'mqttPublishMessage', + 'newRelic', + 'scheduler', + 'senseAppReload', + 'senseAppDump', + 'senseListApps', + 'senseStartTask', + 'slackPostMessage', + ], + additionalProperties: false, + }, + + restServerEndpointsConfig: { + type: 'object', + properties: { + newRelic: { + type: 'object', + properties: { + postNewRelicMetric: { + type: 'object', + properties: { + destinationAccount: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + url: { + type: 'string', + format: 'uri', + }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['destinationAccount', 'url', 'header', 'attribute'], + additionalProperties: false, + }, + postNewRelicEvent: { + type: 'object', + properties: { + destinationAccount: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + url: { + type: 'string', + format: 'uri', + }, + header: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + attribute: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, + }, + required: ['destinationAccount', 'url', 'header', 'attribute'], + additionalProperties: false, + }, + }, + required: ['postNewRelicMetric', 'postNewRelicEvent'], + additionalProperties: false, + }, + }, + required: ['newRelic'], + additionalProperties: false, + }, + + startTaskFilter: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + allowTask: { + type: 'object', + properties: { + taskId: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + tag: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + customProperty: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['taskId', 'tag', 'customProperty'], + additionalProperties: false, + }, + }, + required: ['enable', 'allowTask'], + additionalProperties: false, + }, + + serviceMonitor: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + frequency: { type: 'string' }, + monitor: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + host: { + type: 'string', + format: 'hostname', + }, + services: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + friendlyName: { type: 'string' }, + }, + required: ['name', 'friendlyName'], + additionalProperties: false, + }, + }, + }, + required: ['host', 'services'], + additionalProperties: false, + }, + }, + alertDestination: { + type: 'object', + properties: { + influxDb: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + newRelic: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + email: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + mqtt: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + teams: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + slack: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + webhook: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + }, + required: ['enable'], + additionalProperties: false, + }, + }, + required: ['influxDb', 'newRelic', 'email', 'mqtt', 'teams', 'slack', 'webhook'], + additionalProperties: false, + }, + }, + required: ['enable', 'frequency', 'monitor', 'alertDestination'], + additionalProperties: false, + }, + + qlikSenseCloud: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + event: { + type: 'object', + properties: { + mqtt: { + type: 'object', + properties: { + tenant: { + type: 'object', + properties: { + id: { type: 'string' }, + tenantUrl: { + type: 'string', + format: 'uri', + }, + authType: { + type: 'string', + enum: ['jwt'], + transform: ['trim', 'toLowerCase'], + }, + auth: { + type: 'object', + properties: { + jwt: { + type: 'object', + properties: { + token: { type: 'string' }, + }, + required: ['token'], + additionalProperties: false, + }, + }, + required: ['jwt'], + additionalProperties: false, + }, + qlikSenseUrls: { + type: 'object', + properties: { + qmc: { + type: 'string', + format: 'uri', + }, + hub: { + type: 'string', + }, + }, + required: ['qmc', 'hub'], + additionalProperties: false, + }, + comment: { type: 'string' }, + alert: { + type: 'object', + properties: { + teamsNotification: { + type: 'object', + properties: { + reloadAppFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + alertEnableByTag: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { type: 'string' }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + basicContentOnly: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + }, + required: [ + 'enable', + 'alertEnableByTag', + 'basicContentOnly', + 'webhookURL', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + ], + additionalProperties: false, + }, + }, + required: ['reloadAppFailure'], + additionalProperties: false, + }, + slackNotification: { + type: 'object', + properties: { + reloadAppFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + alertEnableByTag: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { type: 'string' }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + basicContentOnly: { type: 'boolean' }, + webhookURL: { + type: 'string', + format: 'uri', + }, + channel: { type: 'string' }, + messageType: { + type: 'string', + enum: ['basic', 'formatted'], + transform: ['trim', 'toLowerCase'], + }, + basicMsgTemplate: { type: 'string' }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + templateFile: { type: 'string' }, + fromUser: { type: 'string' }, + iconEmoji: { type: 'string' }, + }, + required: [ + 'enable', + 'alertEnableByTag', + 'basicContentOnly', + 'webhookURL', + 'channel', + 'messageType', + 'basicMsgTemplate', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'templateFile', + 'fromUser', + 'iconEmoji', + ], + additionalProperties: false, + }, + }, + required: ['reloadAppFailure'], + additionalProperties: false, + }, + emailNotification: { + type: 'object', + properties: { + reloadAppFailure: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + alertEnableByTag: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + tag: { type: 'string' }, + }, + required: ['enable', 'tag'], + additionalProperties: false, + }, + appOwnerAlert: { + type: 'object', + properties: { + enable: { type: 'boolean' }, + includeOwner: { + type: 'object', + properties: { + includeAll: { type: 'boolean' }, + user: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['includeAll', 'user'], + additionalProperties: false, + }, + excludeOwner: { + type: 'object', + properties: { + user: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: ['user'], + additionalProperties: false, + }, + }, + required: ['enable', 'includeOwner', 'excludeOwner'], + additionalProperties: false, + }, + rateLimit: { type: 'number' }, + headScriptLogLines: { type: 'number' }, + tailScriptLogLines: { type: 'number' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high'], + transform: ['trim', 'toLowerCase'], + }, + subject: { type: 'string' }, + bodyFileDirectory: { type: 'string' }, + htmlTemplateFile: { type: 'string' }, + fromAddress: { type: 'string' }, + recipients: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + }, + required: [ + 'enable', + 'alertEnableByTag', + 'appOwnerAlert', + 'rateLimit', + 'headScriptLogLines', + 'tailScriptLogLines', + 'priority', + 'subject', + 'bodyFileDirectory', + 'htmlTemplateFile', + 'fromAddress', + 'recipients', + ], + additionalProperties: false, + }, + }, + required: ['reloadAppFailure'], + additionalProperties: false, + }, + }, + required: ['teamsNotification', 'slackNotification', 'emailNotification'], + additionalProperties: false, + }, + }, + required: ['id', 'tenantUrl', 'authType', 'auth', 'qlikSenseUrls', 'comment', 'alert'], + additionalProperties: false, + }, + }, + required: ['tenant'], + additionalProperties: false, + }, + }, + required: ['mqtt'], + additionalProperties: false, + }, + }, + required: ['enable', 'event'], + }, + + cert: { + type: 'object', + properties: { + clientCert: { type: 'string' }, + clientCertKey: { type: 'string' }, + clientCertCA: { type: 'string' }, + }, + required: ['clientCert', 'clientCertKey', 'clientCertCA'], + additionalProperties: false, + }, + + configEngine: { + type: 'object', + properties: { + engineVersion: { type: 'string' }, + host: { + type: 'string', + format: 'hostname', + }, + port: { type: 'number' }, + useSSL: { type: 'boolean' }, + headers: { + type: 'object', + properties: {}, + required: [], + additionalProperties: true, + }, + rejectUnauthorized: { type: 'boolean' }, + }, + required: ['engineVersion', 'host', 'port', 'useSSL', 'headers', 'rejectUnauthorized'], + additionalProperties: false, + }, + + configQRS: { + type: 'object', + properties: { + authentication: { type: 'string' }, + host: { + type: 'string', + format: 'hostname', + }, + port: { type: 'number' }, + useSSL: { type: 'boolean' }, + headerKey: { type: 'string' }, + headerValue: { type: 'string' }, + rejectUnauthorized: { type: 'boolean' }, + }, + required: ['authentication', 'host', 'useSSL', 'port', 'headerKey', 'headerValue', 'rejectUnauthorized'], + additionalProperties: false, + }, + + configDirectories: { + type: 'object', + properties: { + qvdPath: { type: 'string' }, + }, + }, + }, + required: [ + 'logLevel', + 'fileLogging', + 'logDirectory', + 'anonTelemetry', + 'configVisualisation', + 'heartbeat', + 'dockerHealthCheck', + 'uptimeMonitor', + 'thirdPartyToolsCredentials', + 'influxDb', + 'scriptLog', + 'qlikSenseUrls', + 'qlikSenseVersion', + 'qlikSenseLicense', + 'teamsNotification', + 'slackNotification', + 'emailNotification', + 'incidentTool', + 'webhookNotification', + 'scheduler', + 'keyValueStore', + 'mqttConfig', + 'udpServerConfig', + 'restServerConfig', + 'fileCopyApprovedDirectories', + 'fileMoveApprovedDirectories', + 'fileDeleteApprovedDirectories', + 'restServerApiDocGenerate', + 'restServerEndpointsEnable', + 'restServerEndpointsConfig', + 'startTaskFilter', + 'serviceMonitor', + 'qlikSenseCloud', + 'cert', + 'configEngine', + 'configQRS', + 'configDirectories', + ], + additionalProperties: true, + }, + }, + required: ['Butler'], + additionalProperties: false, +}; From 2769553e4ea10367d37c6d4e097299aaf6854c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Fri, 27 Sep 2024 19:06:44 +0000 Subject: [PATCH 06/14] fix(slack-alert): Align Slack template for QSEoW alerts with QS Cloud ditto Fixes #1238 --- src/config/production_template.yaml | 8 ++++---- ...-reload.handlebars => aborted-reload-qseow.handlebars} | 0 ...d-reload.handlebars => failed-reload-qseow.handlebars} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename src/config/slack_templates/{aborted-reload.handlebars => aborted-reload-qseow.handlebars} (100%) rename src/config/slack_templates/{failed-reload.handlebars => failed-reload-qseow.handlebars} (100%) diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 0442067b..7832711e 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -291,7 +291,7 @@ Butler: enable: false restMessage: webhookURL: # Webhook to use when sending basic Slack messages via Butler's REST API - reloadTaskFailure: + reloadTaskFailure: # Reload task failed in QSEoW enable: false webhookURL: channel: sense-task-failure # Slack channel to which task failure notifications are sent @@ -300,10 +300,10 @@ Butler: rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 - templateFile: /path/to/slack/template/directory/failed-reload.handlebars + templateFile: /path/to/slack/template/directory/failed-reload-qseow.handlebars fromUser: Qlik Sense iconEmoji: ':ghost:' - reloadTaskAborted: + reloadTaskAborted: # Reload task aborted in QSEoW enable: false webhookURL: channel: sense-task-aborted # Slack channel to which task stopped notifications are sent @@ -312,7 +312,7 @@ Butler: rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 - templateFile: /path/to/slack/template/directory/aborted-reload.handlebars + templateFile: /path/to/slack/template/directory/aborted-reload-qseow.handlebars fromUser: Qlik Sense iconEmoji: ':ghost:' serviceStopped: diff --git a/src/config/slack_templates/aborted-reload.handlebars b/src/config/slack_templates/aborted-reload-qseow.handlebars similarity index 100% rename from src/config/slack_templates/aborted-reload.handlebars rename to src/config/slack_templates/aborted-reload-qseow.handlebars diff --git a/src/config/slack_templates/failed-reload.handlebars b/src/config/slack_templates/failed-reload-qseow.handlebars similarity index 100% rename from src/config/slack_templates/failed-reload.handlebars rename to src/config/slack_templates/failed-reload-qseow.handlebars From 4f46e6087641ff925b67e7c151bc1b9fc31a6d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 07:21:03 +0000 Subject: [PATCH 07/14] fix(ms-teams)!: Use MS Power Automate for sending Teams messges Implements #1239 --- src/config/production_template.yaml | 4 +- ...lebars => aborted-reload-qseow.handlebars} | 0 .../failed-reload-qseow.handlebars | 203 ++++++++++++++++++ .../teams_templates/failed-reload.handlebars | 89 -------- src/lib/msteams_notification.js | 12 +- 5 files changed, 211 insertions(+), 97 deletions(-) rename src/config/teams_templates/{aborted-reload.handlebars => aborted-reload-qseow.handlebars} (100%) create mode 100644 src/config/teams_templates/failed-reload-qseow.handlebars delete mode 100644 src/config/teams_templates/failed-reload.handlebars diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 7832711e..3612fa7a 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -262,7 +262,7 @@ Butler: rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 - templateFile: /path/to/teams/template/directory/failed-reload.handlebars + templateFile: /path/to/teams/template/directory/failed-reload-qseow.handlebars reloadTaskAborted: enable: false webhookURL: @@ -271,7 +271,7 @@ Butler: rateLimit: 300 # Min seconds between emails for a given taskID. Defaults to 5 minutes. headScriptLogLines: 10 tailScriptLogLines: 10 - templateFile: /path/to/teams/template/directory/aborted-reload.handlebars + templateFile: /path/to/teams/template/directory/aborted-reload-qseow.handlebars serviceStopped: webhookURL: messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. diff --git a/src/config/teams_templates/aborted-reload.handlebars b/src/config/teams_templates/aborted-reload-qseow.handlebars similarity index 100% rename from src/config/teams_templates/aborted-reload.handlebars rename to src/config/teams_templates/aborted-reload-qseow.handlebars diff --git a/src/config/teams_templates/failed-reload-qseow.handlebars b/src/config/teams_templates/failed-reload-qseow.handlebars new file mode 100644 index 00000000..a3c603c3 --- /dev/null +++ b/src/config/teams_templates/failed-reload-qseow.handlebars @@ -0,0 +1,203 @@ +{ + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": null, + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.3", + "msteams": { + "width": "Full" + }, + "body": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "Qlik Sense reload task failed", + "style": "heading", + "wrap": true + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "Image", + "style": "person", + "url": "https://raw.githubusercontent.com/ptarmiganlabs/butler/master/icon.png", + "altText": "Butler the Bot", + "size": "medium" + } + ], + "width": "auto" + }, + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "weight": "bolder", + "text": "Butler the Bot", + "wrap": true + }, + { + "type": "TextBlock", + "spacing": "none", + "text": "", + "isSubtle": true, + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "FactSet", + "spacing": "large", + "facts": [ + { + "title": "App name:", + "value": "{{appName}}" + }, + { + "title": "App ID:", + "value": "{{appId}}" + }, + { + "title": "Task name:", + "value": "{{taskName}}" + }, + { + "title": "Task ID:", + "value": "{{tasId}}" + }, + { + "title": "App owner:", + "value": "{{appOwnerName}}" + }, + { + "title": "App owner user:", + "value": "{{appOwnerUserDirectory}}/{{appOwnerUserId}}" + }, + { + "title": "App owner email:", + "value": "{{appOwnerEmail}}" + }, + { + "title": "Reload started:", + "value": "{{executionStartTime.startTimeLocal1}}" + }, + { + "title": "Duration:", + "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" + }, + { + "title": "Reload ended:", + "value": "{{executionStopTime.stopTimeLocal1}}" + }, + { + "title": "User starting the reload:", + "value": "{{executionStartTime.startTimeLocal1}}" + } + ] + }, + + { + "type": "ActionSet", + "spacing": "extraLarge", + "separator": true, + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Open QMC", + "tooltip": "Open management console in Qlik Sense", + "url": "{{qlikSenseQMC}}", + "role": "button" + }, + { + "type": "Action.OpenUrl", + "title": "Open hub", + "tooltip": "Open hub Qlik Sense", + "url": "{{qlikSenseQMC}}", + "role": "button" + } + ] + }, + + + { + "type": "Container", + "spacing": "extraLarge", + "style": "emphasis", + "items": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "Details", + "style": "heading" + }, + { + "type": "FactSet", + "separator": true, + "facts": [ + { + "title": "Execution result:", + "value": "{{executionStatusText}}" + }, + { + "title": "Execution result code:", + "value": "{{executionStatusNum}}" + }, + { + "title": "Log timestamp:", + "value": "{{logTimeStamp}}" + }, + { + "title": "Log message:", + "value": "{{logMessage}}" + } + ] + }, + { + "type": "CodeBlock", + "codeSnippet": "{{executionDetailsConcatenated}}" + } + ] + }, + { + "type": "Container", + "spacing": "extraLarge", + "style": "emphasis", + "items": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": "End of script log", + "style": "heading" + }, + { + "type": "TextBlock", + "size": "small", + "weight": "normal", + "text": "Last {{scriptLogTailCount}} rows shown. The script log contains {{scriptLogSize}} rows in total.", + "style": "heading" + }, + { + "type": "CodeBlock", + "codeSnippet": "{{scriptLogTail}}" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/config/teams_templates/failed-reload.handlebars b/src/config/teams_templates/failed-reload.handlebars deleted file mode 100644 index 770f3a51..00000000 --- a/src/config/teams_templates/failed-reload.handlebars +++ /dev/null @@ -1,89 +0,0 @@ -{ - "@type": "MessageCard", - "@context": "https://schema.org/extensions", - "themeColor": "0076D7", - "summary": "Qlik Sense reload task failed", - "sections": [{ - "activityTitle": "# Reload task failed: '{{taskName}}'", - "activitySubtitle": "Reload attempt was done on server: {{executingNodeName}}", - "facts": [{ - "name": "App name", - "value": "{{appName}}" - },{ - "name": "App ID", - "value": "{{appId}}" - },{ - "name": "Task Name", - "value": "{{taskName}}" - },{ - "name": "Task ID", - "value": "{{taskId}}" - },{ - "name": "App owner", - "value": "{{appOwnerName}}" - },{ - "name": "App owner user", - "value": "{{appOwnerUserDirectory}}/{{appOwnerUserId}}" - },{ - "name": "App owner email", - "value": "{{appOwnerEmail}}" - }], - "markdown": true - }, { - "facts": [{ - "name": "User starting the reload", - "value": "{{user}}" - },{ - "name": "Duration", - "value": "{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds" - },{ - "name": "Execution started", - "value": "{{executionStartTime.startTimeLocal1}}" - },{ - "name": "Execution ended", - "value": "{{executionStartTime.stopTimeLocal1}}" - },{ - "name": "Execution result", - "value": "{{executionStatusText}}" - },{ - "name": "Execution result code", - "value": "{{executionStatusNum}}" - },{ - "name": "Log timestamp", - "value": "{{logTimeStamp}}" - },{ - "name": "Log message", - "value": "{{logMessage}}" - }], - "markdown": true - },{ - "activityTitle": "History", - "text": "{{executionDetailsConcatenated}}", - "markdown": true - },{ - "activityTitle": "Beginning of script log (up to {{scriptLogHeadCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogHead}}", - "markdown": true - },{ - "activityTitle": "End of script log (up to {{scriptLogTailCount}} lines)", - "activitySubtitle": "The script log contains {{scriptLogSize}} characters in total.", - "text": "{{scriptLogTail}}", - "markdown": true - }], - "potentialAction": [{ - "@type": "OpenUri", - "name": "Qlik Sense QMC", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseQMC}}" - }] - },{ - "@type": "OpenUri", - "name": "Qlik Sense Hub", - "targets": [{ - "os": "default", - "uri": "{{qlikSenseHub}}" - }] - }] -} diff --git a/src/lib/msteams_notification.js b/src/lib/msteams_notification.js index 8a1ca50b..cd1dba80 100644 --- a/src/lib/msteams_notification.js +++ b/src/lib/msteams_notification.js @@ -483,16 +483,16 @@ export function sendReloadTaskFailureNotificationTeams(reloadParams) { executionDetails: scriptLogData.executionDetails, executionDetailsConcatenated: scriptLogData.executionDetailsConcatenated .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogSize: scriptLogData.scriptLogSize, scriptLogHead: scriptLogData.scriptLogHead .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTail: scriptLogData.scriptLogTail .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTailCount: scriptLogData.scriptLogTailCount, scriptLogHeadCount: scriptLogData.scriptLogHeadCount, @@ -629,16 +629,16 @@ export function sendReloadTaskAbortedNotificationTeams(reloadParams) { executionDetails: scriptLogData.executionDetails, executionDetailsConcatenated: scriptLogData.executionDetailsConcatenated .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogSize: scriptLogData.scriptLogSize, scriptLogHead: scriptLogData.scriptLogHead .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTail: scriptLogData.scriptLogTail .replace(/([\r])/gm, '') - .replace(/([\n])/gm, '\\n\\n') + .replace(/([\n])/gm, '\\n') .replace(/([\t])/gm, '\\t'), scriptLogTailCount: scriptLogData.scriptLogTailCount, scriptLogHeadCount: scriptLogData.scriptLogHeadCount, From 7f57a3b79f996ea9ae7ef9f7cb797b9bcf0762ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 07:41:48 +0000 Subject: [PATCH 08/14] Fix config file used when generating API docs --- src/config/config-gen-api-docs.yaml | 142 ++++++++++++++++++++++------ src/config/production_template.yaml | 2 +- 2 files changed, 116 insertions(+), 28 deletions(-) diff --git a/src/config/config-gen-api-docs.yaml b/src/config/config-gen-api-docs.yaml index c430c173..0361ba68 100644 --- a/src/config/config-gen-api-docs.yaml +++ b/src/config/config-gen-api-docs.yaml @@ -22,7 +22,7 @@ Butler: # Heartbeats can be used to send "I'm alive" messages to any other tool, e.g. an infrastructure monitoring tool heartbeat: enable: false - remoteURL: ... + remoteURL: http://10.11.12.13 frequency: every 30 seconds # https://bunkat.github.io/later/parsers.html # Docker health checks are used when running Butler as a Docker container. @@ -83,7 +83,7 @@ Butler: # Settingd for InfluxDB influxDb: enable: false - hostIP: ... + hostIP: 10.11.12.13 hostPort: 8086 auth: enable: false @@ -133,8 +133,8 @@ Butler: # Qlik Sense related links used in notification messages qlikSenseUrls: - qmc: ... - hub: ... + qmc: http://10.11.12.13 + hub: http://10.11.12.13 # Settings for monitoring Qlik Sense version info # Version info is retrieved from the hostname:9032/v1/systeminfo endpoint in Qlik Sense @@ -239,7 +239,7 @@ Butler: enable: false reloadTaskFailure: enable: false - webhookURL: ... + webhookURL: http://10.11.12.13 messageType: formatted # formatted / basic basicMsgTemplate: 'Qlik Sense reload failed: "{{taskName}}"' # Only needed if message type = basic rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. @@ -248,7 +248,7 @@ Butler: templateFile: ... reloadTaskAborted: enable: false - webhookURL: ... + webhookURL: http://10.11.12.13 messageType: formatted # formatted / basic basicMsgTemplate: 'Qlik Sense reload aborted: "{{taskName}}"' # Only needed if message type = basic rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. @@ -256,13 +256,13 @@ Butler: tailScriptLogLines: 10 templateFile: ... serviceStopped: - webhookURL: ... + webhookURL: http://10.11.12.13 messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Windows service stopped: "{{serviceName}}" on host "{{host}}"' # Only needed if message type = basic rateLimit: 5 # Min seconds between messages for a given Windows service. Defaults to 5 minutes. templateFile: ... serviceStarted: - webhookURL: ... + webhookURL: http://10.11.12.13 messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Windows service started: "{{serviceName}}" on host "{{host}}"' # Only needed if message type = basic rateLimit: 5 # Min seconds between messages for a given Windows service. Defaults to 5 minutes. @@ -272,10 +272,10 @@ Butler: slackNotification: enable: false restMessage: - webhookURL: # Webhook to use when sending basic Slack messages via Butler's REST API + webhookURL: http://10.11.12.13 # Webhook to use when sending basic Slack messages via Butler's REST API reloadTaskFailure: enable: false - webhookURL: ... + webhookURL: http://10.11.12.13 # channel: sense-task-failure # Slack channel to which task failure notifications are sent channel: ... # Slack channel to which task failure notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. @@ -289,7 +289,7 @@ Butler: iconEmoji: ':ghost:' reloadTaskAborted: enable: false - webhookURL: ... + webhookURL: http://10.11.12.13 channel: sense-task-aborted # Slack channel to which task stopped notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Qlik Sense PROD reload aborted: "{{taskName}}"' # Only needed if message type = basic @@ -300,7 +300,7 @@ Butler: fromUser: Qlik Sense iconEmoji: ':ghost:' serviceStopped: - webhookURL: ... + webhookURL: http://10.11.12.13 channel: qliksense-service-alert # Slack channel to which Windows service stopped notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Windows service stopped: "{{serviceName}}" on host "{{host}}"' # Only needed if message type = basic @@ -309,7 +309,7 @@ Butler: fromUser: Qlik Sense iconEmoji: ':ghost:' serviceStarted: - webhookURL: ... + webhookURL: http://10.11.12.13 channel: qliksense-service-alert # Slack channel to which Windows service stopped notifications are sent messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. basicMsgTemplate: 'Windows service started: "{{serviceName}}" on host "{{host}}"' # Only needed if message type = basic @@ -413,7 +413,7 @@ Butler: recipients: # - ... smtp: - host: ... + host: 10.11.12.13 port: 465 secure: true tls: @@ -431,7 +431,7 @@ Butler: incidentTool: signl4: enable: false - url: ... + url: http://10.11.12.13 reloadTaskFailure: enable: true rateLimit: 15 # Min seconds between alerts for a given taskID. Defaults to 5 minutes. @@ -441,10 +441,6 @@ Butler: includeAll: false appId: # - ... - appNameRegEx: - excludeApp: - appId: - appNameRegEx: reloadTaskAborted: enable: true rateLimit: 15 # Min seconds between alerts for a given taskID. Defaults to 5 minutes. @@ -453,10 +449,6 @@ Butler: includeApp: includeAll: true appId: - appNameRegEx: - excludeApp: - appId: - appNameRegEx: newRelic: enable: false destinationAccount: @@ -655,7 +647,7 @@ Butler: mqttConfig: enable: false - brokerHost: ... + brokerHost: 10.11.12.13 brokerPort: 1883 azureEventGrid: enable: false # If set to true, Butler will connect to an Azure Event Grid MQTT Broker, using brokerHost and brokerPort above @@ -676,10 +668,22 @@ Butler: serviceStatusTopic: qliksense/service_status qlikSenseServerLicenseTopic: qliksense/butler/qliksense_server_license # Topic to which Sense server license info is published qlikSenseServerLicenseExpireTopic: qliksense/butler/qliksense_server_license_expire # Topic to which Sense server license expiration alerts are published + qlikSenseCloud: # MQTT settings for Qlik Sense Cloud integration + event: + mqttForward: # QS Cloud events forwarded to MQTT topics, which Butler will subscribe to + enable: false + broker: # Settings for MQTT broker to which QS Cloud events are forwarded + host: mqttbroker.company.com + port: 1234 + username: + password: + topic: + subscriptionRoot: qscloud/# # Topic that Butler will subscribe to + appReload: qscloud/app/reload udpServerConfig: enable: false - serverHost: ... + serverHost: 10.11.12.13 portTaskFailure: 9998 restServerConfig: @@ -812,6 +816,90 @@ Butler: webhook: enable: false + qlikSenseCloud: # Settings for Qlik Sense Cloud integration + enable: false + event: + mqtt: # Which QS Cloud tenant should Butler SOS receive events from, in the form of MQTT messages? + tenant: + id: sometenant.qlikcloud.com + tenantUrl: https://sometenant.qlikcloud.com + authType: jwt # Authentication type used to connect to the tenant. Valid options are "jwt" + auth: + jwt: + token: # JWT token used to authenticate Butler SOS when connecting to the tenant + # Qlik Sense Cloud related links used in notification messages + qlikSenseUrls: + qmc: http://someurl.mycompany.com + hub: http://someurl.mycompany.com + comment: This is a comment describing the tenant and its settings # Informational only + alert: + # Settings for notifications and messages sent to MS Teams + teamsNotification: + reloadAppFailure: + enable: false + alertEnableByTag: + enable: false + tag: Butler - Send Teams alert if app reload fails + basicContentOnly: false + webhookURL: http://10.11.12.13 + + messageType: formatted # formatted / basic + basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"' # Only needed if message type = basic + rateLimit: 15 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 15 + tailScriptLogLines: 15 + templateFile: /path/to/teams_templates/failed-reload-qscloud-workflow.handlebars + + # Settings for notifications and messages sent to Slack + slackNotification: + reloadAppFailure: + enable: false + alertEnableByTag: + enable: false + tag: Butler - Send Slack alert if app reload fails + basicContentOnly: false + webhookURL: http://10.11.12.13 + channel: sense-task-failure # Slack channel to which task failure notifications are sent + messageType: formatted # formatted / basic. Formatted means that template file below will be used to create the message. + basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"' # Only needed if message type = basic + rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 10 + tailScriptLogLines: 20 + templateFile: /path/to/slack_templates/failed-reload-qscloud.handlebars + fromUser: Qlik Sense + iconEmoji: ':ghost:' + + # Settings needed to send email notifications when for example reload tasks fail. + # Reload failure notifications assume a log appender is configured in Sense AND that the UDP server in Butler is running. + emailNotification: + reloadAppFailure: + enable: false # Enable/disable app reload failed notifications via email + alertEnableByTag: + enable: false + tag: Butler - Send email if app reload fails + appOwnerAlert: + enable: false # Should app owner get notification email (assuming email address is available in Sense)? + includeOwner: + includeAll: true # true = Send notification to all app owners except those in exclude list + # false = Send notification to app owners in the include list + user: # Array of app owner email addresses that should get notifications + # - email: anna@somecompany.com + # - email: joe@somecompany.com + excludeOwner: + user: + # - email: daniel@somecompany.com + rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes. + headScriptLogLines: 15 + tailScriptLogLines: 25 + priority: high # high/normal/low + subject: '❌ Qlik Sense reload failed: "{{taskName}}"' + bodyFileDirectory: /path/to//email_templates + htmlTemplateFile: failed-reload-qscloud + fromAddress: Qlik Sense (no-reply) + recipients: + # - emma@somecompany.com + # - patrick@somecompany.com + # Certificates to use when connecting to Sense. Get these from the Certificate Export in QMC. cert: clientCert: ... @@ -820,7 +908,7 @@ Butler: configEngine: engineVersion: 12.612.0 # Qlik Associative Engine version to use with Enigma.js. Works with Feb 2020 and others - host: ... + host: 10.11.12.13 port: 4747 useSSL: true headers: @@ -830,7 +918,7 @@ Butler: configQRS: authentication: certificates - host: ... + host: 10.11.12.13 useSSL: true port: 4242 headerKey: X-Qlik-User # Header used to identify what user connection to QRS is made as diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 3612fa7a..ba5adf5f 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -27,7 +27,7 @@ Butler: # Should Butler start a web server that serves an obfuscated view of the Butler config file? configVisualisation: - enable: true + enable: true host: localhost # Hostname or IP address where the web server will listen. Should be localhost in most cases. port: 3100 # Port where the web server will listen. Change if port 3100 is already in use. obfuscate: true # Should the config file shown in the web UI be obfuscated? From 7f57bcde52f8595c8d9e36240b0822b3f5d599c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 08:03:40 +0000 Subject: [PATCH 09/14] Remove leftover, old config file assert file --- src/lib/assert/assert_config_file copy.js | 5437 --------------------- 1 file changed, 5437 deletions(-) delete mode 100644 src/lib/assert/assert_config_file copy.js diff --git a/src/lib/assert/assert_config_file copy.js b/src/lib/assert/assert_config_file copy.js deleted file mode 100644 index 19d52b14..00000000 --- a/src/lib/assert/assert_config_file copy.js +++ /dev/null @@ -1,5437 +0,0 @@ -import QrsInteract from 'qrs-interact'; -import yaml from 'js-yaml'; -import { getReloadTasksCustomProperties } from '../../qrs_util/task_cp_util.js'; - -// Veriify InfluxDb related settings in the config file -export const configFileInfluxDbAssert = async (config, configQRS, logger) => { - // ------------------------------------------ - // The custom property specified by - // Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName - // should be present on reload tasks in the Qlik Sense server - - // Only test if the feature in question is enabled in the config file - if ( - config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable') === true && - config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName') && - config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue') - ) { - // Get custom property values - try { - const res1 = await getReloadTasksCustomProperties(config, configQRS, logger); - logger.debug(`ASSERT CONFIG INFLUXDB: The following custom properties are available for reload tasks: ${res1}`); - - // CEnsure that the CP name specified in the config file is found in the list of available CPs - // CP name is case sensitive and found in the "name" property of the CP object - if ( - res1.findIndex((cp) => cp.name === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName')) === - -1 - ) { - logger.error( - `ASSERT CONFIG INFLUXDB: Custom property '${config.get( - 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', - )}' not found in Qlik Sense. Aborting.`, - ); - return false; - } - - // Ensure that the CP value specified in the config file is found in the list of available CP values - // CP value is case sensitive and found in the "choiceValues" array of the CP objects in res1 - const res2 = res1.filter( - (cp) => cp.name === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName'), - )[0].choiceValues; - logger.debug( - `ASSERT CONFIG INFLUXDB: The following values are available for custom property '${config.get( - 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', - )}': ${res2}`, - ); - - if ( - res2.findIndex((cpValue) => cpValue === config.get('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue')) === - -1 - ) { - logger.error( - `ASSERT CONFIG INFLUXDB: Custom property value '${config.get( - 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue', - )}' not found for custom property '${config.get( - 'Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName', - )}'. Aborting.`, - ); - return false; - } - } catch (err) { - logger.error(`ASSERT CONFIG INFLUXDB: ${err}`); - } - } - return true; -}; - -/** - * Verify New Relic settings in the config file - */ -export const configFileNewRelicAssert = async (config, configQRS, logger) => { - // Set up shared Sense repository service configuration - const cfg = { - hostname: config.get('Butler.configQRS.host'), - portNumber: 4242, - certificates: { - certFile: configQRS.certPaths.certPath, - keyFile: configQRS.certPaths.keyPath, - }, - }; - - cfg.headers = { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }; - - const qrsInstance = new QrsInteract(cfg); - - // ------------------------------------------ - // The custom property specified by - // Butler.incidentToo.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName - // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array - - // Only test if the feature in question is enabled in the config file - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName') - ) { - // Get custom property values - try { - logger.debug( - `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - logger.debug( - `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - - const result1 = await qrsInstance.Get( - `custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}'`, - ); - - // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic - - // If the custom property doesn't exist that's a problem.. - if (result1.body.length === 0) { - logger.error( - `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )})' does not exist in Qlik Sense. Aborting.`, - ); - return false; - } - - // If there are no choiceValues that's a problem.. - if ( - result1.body[0].choiceValues === undefined || - result1.body[0].choiceValues === null || - result1.body[0].choiceValues.length === 0 - ) { - logger.warn( - `ASSERT CONFIG NEW RELIC: Custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, - ); - } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { - // New Relic account specified as destination for events, but no account(s) specified in config file or on command line - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for alert events, but no New Relic account(s) specified on either command line or in config file. Aborting,`, - ); - return false; - } else { - // Test each custom property choice value for existence in Butler config file - const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - // eslint-disable-next-line no-restricted-syntax - for (const value of result1.body[0].choiceValues) { - if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}' not found in Butler's config file`, - ); - } - } - } - } catch (err) { - logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); - } - } else { - // eslint-disable-next-line no-lonely-if - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable') && - !config.has( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName', - ) - ) { - logger.error( - `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName"`, - ); - return false; - } - } - - // ------------------------------------------ - // The custom property specified by - // Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName - // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array - - // Only test if the feature in question is enabled in the config file - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName') - ) { - // Get custom property values - try { - logger.debug( - `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - logger.debug( - `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - - const result1 = await qrsInstance.Get( - `custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}'`, - ); - // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic - - // If the custom property doesn't exist that's a problem.. - if (result1.body.length === 0) { - logger.error( - `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )})' does not exist in Qlik Sense. Aborting.`, - ); - return false; - } - - // If there are no choiceValues that's a problem.. - if ( - result1.body[0].choiceValues === undefined || - result1.body[0].choiceValues === null || - result1.body[0].choiceValues.length === 0 - ) { - logger.warn( - `ASSERT CONFIG NEW RELIC: Custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, - ); - } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { - // New Relic account specified as destination for events, but no account(s) specified in config file or on command line - logger.error( - `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for failed reload alert logs, but no New Relic account(s) specified on either command line or in config file. Aborting,`, - ); - return false; - } else { - // Test each custom property choice value for existence in Butler config file - const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - // eslint-disable-next-line no-restricted-syntax - for (const value of result1.body[0].choiceValues) { - if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}' not found in Butler's config file`, - ); - } - } - } - } catch (err) { - logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); - } - } else { - // eslint-disable-next-line no-lonely-if - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable') && - !config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName') - ) { - logger.error( - `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName"`, - ); - return false; - } - } - - // ------------------------------------------ - // The custom property specified by - // Butler.incidentToo.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName - // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array - - // Only test if the feature in question is enabled in the config file - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName') - ) { - // Get custom property values - try { - logger.debug( - `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - logger.debug( - `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - - const result1 = await qrsInstance.Get( - `custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}'`, - ); - // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic - - // If the custom property doesn't exist that's a problem.. - if (result1.body.length === 0) { - logger.error( - `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )})' does not exist in Qlik Sense. Aborting.`, - ); - return false; - } - - // If there are no choiceValues that's a problem.. - if ( - result1.body[0].choiceValues === undefined || - result1.body[0].choiceValues === null || - result1.body[0].choiceValues.length === 0 - ) { - logger.warn( - `ASSERT CONFIG NEW RELIC: Custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, - ); - } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { - // New Relic account specified as destination for events, but no account(s) specified in config file or on command line - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for alert events, but no New Relic account(s) specified on either command line or in config file. Aborting,`, - ); - return false; - } else { - // Test each custom property choice value for existence in Butler config file - const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - // eslint-disable-next-line no-restricted-syntax - for (const value of result1.body[0].choiceValues) { - if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - )}' not found in Butler's config file`, - ); - } - } - } - } catch (err) { - logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); - } - } else { - // eslint-disable-next-line no-lonely-if - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable') && - !config.has( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName', - ) - ) { - logger.error( - `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName"`, - ); - return false; - } - } - - // ------------------------------------------ - // The custom property specified by - // Butler.incidentToo.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName - // should only include values present in the Butler.thirdPartyToolsCredentials.newRelic array - - // Only test if the feature in question is enabled in the config file - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName') - ) { - // Get custom property values - try { - logger.debug( - `ASSERT CONFIG NEW RELIC 1: Custom property name: ${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - logger.debug( - `ASSERT CONFIG NEW RELIC 1: custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}`, - ); - - const result1 = await qrsInstance.Get( - `custompropertydefinition/full?filter=name eq '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}'`, - ); - // The choice values of the custom property should match the values in Butler.thirdPartyToolsCredentials.newRelic - - // If the custom property doesn't exist that's a problem.. - if (result1.body.length === 0) { - logger.error( - `ASSERT CONFIG NEW RELIC: Custom property specified in config file ('${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )})' does not exist in Qlik Sense. Aborting.`, - ); - return false; - } - - // If there are no choiceValues that's a problem.. - if ( - result1.body[0].choiceValues === undefined || - result1.body[0].choiceValues === null || - result1.body[0].choiceValues.length === 0 - ) { - logger.warn( - `ASSERT CONFIG NEW RELIC: Custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}' does not have any values associated with it. New Relic monitoring may not work as a result of this.`, - ); - } else if (config.get('Butler.thirdPartyToolsCredentials.newRelic') === null) { - // New Relic account specified as destination for events, but no account(s) specified in config file or on command line - logger.error( - `ASSERT CONFIG NEW RELIC: New Relic is set as a destination for aborted reload alert logs, but no New Relic account(s) specified on either command line or in config file. Aborting,`, - ); - return false; - } else { - // Test each custom property choice value for existence in Butler config file - const availableNewRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - // eslint-disable-next-line no-restricted-syntax - for (const value of result1.body[0].choiceValues) { - if (availableNewRelicAccounts.findIndex((account) => value === account.accountName) === -1) { - logger.warn( - `ASSERT CONFIG NEW RELIC: New Relic account name '${value}' of custom property '${config.get( - 'Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName', - )}' not found in Butler's config file`, - ); - } - } - } - } catch (err) { - logger.error(`ASSERT CONFIG NEW RELIC: ${err}`); - } - } else { - // eslint-disable-next-line no-lonely-if - if ( - config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && - config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable') && - !config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName') - ) { - logger.error( - `ASSERT CONFIG NEW RELIC: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName"`, - ); - return false; - } - } - - return true; -}; - -// Function to verify that config file is valid YAML -export const configFileYamlAssert = async (configFile) => { - try { - const data = await yaml.load(configFile); - } catch (err) { - console.error(`ASSERT CONFIG: Config file is not valid YAML: ${err}`); - return false; - } - - return true; -}; - -// Function to verify that config variable have same structure as production.yaml file -export const configFileStructureAssert = async (config, logger) => { - let configFileCorrect = true; - - if (!config.has('Butler.logLevel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.logLevel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.fileLogging')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileLogging"'); - configFileCorrect = false; - } - - if (!config.has('Butler.logDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.logDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.anonTelemetry')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.anonTelemetry"'); - configFileCorrect = false; - } - - // Config visualisation setttings - if (!config.has('Butler.configVisualisation.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configVisualisation.obfuscate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configVisualisation.obfuscate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.remoteURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.remoteURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.heartbeat.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.heartbeat.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.dockerHealthCheck.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.dockerHealthCheck.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.dockerHealthCheck.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.logLevel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.logLevel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeInInfluxdb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeInInfluxdb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.destinationAccount')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.destinationAccount"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.header array are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.uptimeMonitor.storeNewRelic.header')) { - const headers = config.get('Butler.uptimeMonitor.storeNewRelic.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error(`ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.header"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerMemoryUsage.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.metric.dynamic.butlerUptime.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.uptimeMonitor.storeNewRelic.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - - if (config.has('Butler.uptimeMonitor.storeNewRelic.attribute.static')) { - const staticAttributes = config.get('Butler.uptimeMonitor.storeNewRelic.attribute.static'); - - if (staticAttributes) { - if (!Array.isArray(staticAttributes)) { - logger.error('ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static" is not an array'); - configFileCorrect = false; - } else { - staticAttributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.uptimeMonitor.storeNewRelic.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.uptimeMonitor.storeNewRelic.attribute.dynamic.butlerVersion.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.thirdPartyToolsCredentials.newRelic are objects with the following properties: - // { - // accountName: 'string', - // insertApiKey: 'string' - // accountId: 123456 - // } - if (config.has('Butler.thirdPartyToolsCredentials.newRelic')) { - const newRelicAccounts = config.get('Butler.thirdPartyToolsCredentials.newRelic'); - - if (newRelicAccounts) { - if (!Array.isArray(newRelicAccounts)) { - logger.error('ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic" is not an array'); - configFileCorrect = false; - } else { - newRelicAccounts.forEach((account, index) => { - if (typeof account !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(account, 'accountName')) { - logger.error( - `ASSERT CONFIG: Missing "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.accountName !== 'string') { - logger.error( - `ASSERT CONFIG: "accountName" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(account, 'insertApiKey')) { - logger.error( - `ASSERT CONFIG: Missing "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.insertApiKey !== 'string') { - logger.error( - `ASSERT CONFIG: "insertApiKey" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(account, 'accountId')) { - logger.error( - `ASSERT CONFIG: Missing "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof account.accountId !== 'number') { - logger.error( - `ASSERT CONFIG: "accountId" property in "Butler.thirdPartyToolsCredentials.newRelic[${index}]" is not a number`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.thirdPartyToolsCredentials.newRelic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.hostIP')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostIP"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.hostPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.hostPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.username')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.username"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.auth.password')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.auth.password"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.dbName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.dbName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.instanceTag')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.instanceTag"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.retentionPolicy.name')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.name"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.retentionPolicy.duration')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.retentionPolicy.duration"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.influxDb.reloadTaskFailure.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.influxDb.reloadTaskFailure.tag.static')) { - const tagsStatic = config.get('Butler.influxDb.reloadTaskFailure.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskFailure.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useAppTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskFailure.tag.dynamic.useTaskTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.allReloadTasks.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.customPropertyName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.byCustomProperty.enabledValue"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.influxDb.reloadTaskSuccess.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.influxDb.reloadTaskSuccess.tag.static')) { - const tagsStatic = config.get('Butler.influxDb.reloadTaskSuccess.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.influxDb.reloadTaskSuccess.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.static"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useAppTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.influxDb.reloadTaskSuccess.tag.dynamic.useTaskTags"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scriptLog.storeOnDisk.reloadTaskFailure.logDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseUrls.qmc')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.qmc"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseUrls.hub')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseUrls.hub"'); - configFileCorrect = false; - } - - // Qlik Sense version monitoring - if (!config.has('Butler.qlikSenseVersion.versionMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseVersion.versionMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Qlik Sense server license monitoring - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.alert.thresholdDays"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.enabled"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static" is not an array', - ); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.enabled"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendRecurring.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.mqtt.sendAlert.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.enabled"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendRecurring.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.serverLicenseMonitor.destination.webhook.sendAlert.enable"', - ); - configFileCorrect = false; - } - - // Qlik Sense access license monitoring - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.frequency"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enabled"'); - configFileCorrect = false; - } - - // Release Qlik Sense licenses - if (!config.has('Butler.qlikSenseLicense.licenseRelease.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.enable"'); - configFileCorrect = false; - } - - // License release dry run - if (!config.has('Butler.qlikSenseLicense.licenseRelease.dryRun')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.dryRun"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.frequency"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.user are objects with the following properties: - // { - // userDir: 'string' - // userId: 'string', - // } - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.user')) { - const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); - - if (neverReleaseUsers) { - if (!Array.isArray(neverReleaseUsers)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user" is not an array'); - configFileCorrect = false; - } else { - neverReleaseUsers.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userDir')) { - logger.error( - `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userDir !== 'string') { - logger.error( - `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.user"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.tag objects with the following properties: - // - 'string' - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag')) { - const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); - - if (neverReleaseTags) { - if (!Array.isArray(neverReleaseTags)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag" is not an array'); - configFileCorrect = false; - } else { - neverReleaseTags.forEach((tag, index) => { - if (typeof tag !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag"'); - configFileCorrect = false; - } - - // The custom properties specified in the Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty array - // should meet the following requirements: - // - Each array item should be an object with the following properties: - // { - // name: 'string', - // value: 'string' - // } - - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory objects with the following properties: - // - 'string' - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory')) { - const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); - - if (neverReleaseUserDirectories) { - if (!Array.isArray(neverReleaseUserDirectories)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory" is not an array'); - configFileCorrect = false; - } else { - neverReleaseUserDirectories.forEach((userDirectory, index) => { - if (typeof userDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory"'); - configFileCorrect = false; - } - - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive')) { - const inactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive'); - - if (inactive) { - if (typeof inactive !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(inactive.toLowerCase())) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" must be either "Yes" or "No"'); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive"'); - configFileCorrect = false; - } - - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked')) { - const blocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked'); - - if (blocked) { - if (typeof blocked !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(blocked.toLowerCase())) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" must be either "Yes" or "No"'); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked"'); - configFileCorrect = false; - } - - // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally meets the following requirements: - // - Value is either Yes or No - // - Disregard case - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')) { - const removedExternally = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally'); - - if (removedExternally) { - if (typeof removedExternally !== 'string') { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" is not a string'); - configFileCorrect = false; - } else if (!['yes', 'no', 'ignore'].includes(removedExternally.toLowerCase())) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" must be either "Yes" or "No"', - ); - configFileCorrect = false; - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.enable"'); - configFileCorrect = false; - } - - if (config.has('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static')) { - const tagsStatic = config.get('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static'); - - if (tagsStatic) { - if (!Array.isArray(tagsStatic)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static" is not an array'); - configFileCorrect = false; - } else { - tagsStatic.forEach((tag, index) => { - if (typeof tag !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(tag, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(tag, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof tag.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static"'); - configFileCorrect = false; - } - - // Teams notifications - if (!config.has('Butler.teamsNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskFailure.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskFailure.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStopped.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStopped.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.teamsNotification.serviceStarted.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.teamsNotification.serviceStarted.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.restMessage.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.restMessage.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskFailure.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskFailure.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.reloadTaskAborted.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.reloadTaskAborted.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStopped.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStopped.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.webhookURL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.webhookURL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.channel')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.channel"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.messageType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.messageType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.basicMsgTemplate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.basicMsgTemplate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.templateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.templateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.fromUser')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.fromUser"'); - configFileCorrect = false; - } - - if (!config.has('Butler.slackNotification.serviceStarted.iconEmoji')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.slackNotification.serviceStarted.iconEmoji"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enabledValue"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.alertEnabledByEmailAddress.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskAborted.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskAborted.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.reloadTaskAborted.recipients')) { - const recipients = config.get('Butler.emailNotification.reloadTaskAborted.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskAborted.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskAborted.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user are objects with the following properties: - // { - // directory: 'string', - // userId: 'string' - // } - if (config.has('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user')) { - const users = config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user'); - - if (users) { - if (!Array.isArray(users)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user" is not an array'); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'directory')) { - logger.error( - `ASSERT CONFIG: Missing "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.directory !== 'string') { - logger.error( - `ASSERT CONFIG: "directory" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { - logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof user.userId !== 'string') { - logger.error( - `ASSERT CONFIG: "userId" property in "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enabledValue"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.alertEnabledByEmailAddress.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.headScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.headScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.tailScriptLogLines')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.tailScriptLogLines"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.reloadTaskFailure.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.reloadTaskFailure.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.reloadTaskFailure.recipients')) { - const recipients = config.get('Butler.emailNotification.reloadTaskFailure.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.reloadTaskFailure.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.reloadTaskFailure.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStopped.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.serviceStopped.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.serviceStopped.recipients')) { - const recipients = config.get('Butler.emailNotification.serviceStopped.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStopped.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStopped.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.priority')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.priority"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.subject')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.subject"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.bodyFileDirectory')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.bodyFileDirectory"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.htmlTemplateFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.htmlTemplateFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.serviceStarted.fromAdress')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.fromAdress"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.emailNotification.serviceStarted.recipients are objects with the following properties: - // - 'string' - if (config.has('Butler.emailNotification.serviceStarted.recipients')) { - const recipients = config.get('Butler.emailNotification.serviceStarted.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error('ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients" is not an array'); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.emailNotification.serviceStarted.recipients[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.serviceStarted.recipients"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.secure')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.secure"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.serverName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.serverName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.ignoreTLS')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.ignoreTLS"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.requireTLS')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.requireTLS"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.tls.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.tls.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.user')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.user"'); - configFileCorrect = false; - } - - if (!config.has('Butler.emailNotification.smtp.auth.password')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.emailNotification.smtp.auth.password"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.url"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.serviceName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.serviceName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.severity')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.severity"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.includeAll"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId')) { - const appIds = config.get('Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId'); - - if (appIds) { - if (!Array.isArray(appIds)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId" is not an array'); - configFileCorrect = false; - } else { - appIds.forEach((appId, index) => { - if (typeof appId !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskFailure.includeApp.appId"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.serviceName')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.serviceName"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.signl4.reloadTaskAborted.severity')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.signl4.reloadTaskAborted.severity"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.event are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.destinationAccount.event')) { - const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.event'); - - if (destinationAccount) { - if (!Array.isArray(destinationAccount)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event" is not an array'); - configFileCorrect = false; - } else { - destinationAccount.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.event[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.event"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.destinationAccount.log are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.destinationAccount.log')) { - const destinationAccount = config.get('Butler.incidentTool.newRelic.destinationAccount.log'); - - if (destinationAccount) { - if (!Array.isArray(destinationAccount)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log" is not an array'); - configFileCorrect = false; - } else { - destinationAccount.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.incidentTool.newRelic.destinationAccount.log[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.destinationAccount.log"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.url.event')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.url.event"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.event.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.destination.log.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskFailure.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.event.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.byCustomProperty.customPropertyName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.enable"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account')) { - const accounts = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.sendToAccount.always.account"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useAppTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.destination.log.attribute.dynamic.useTaskTags"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.reloadTaskAborted.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount')) { - const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount" is not an array', - ); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.sendToAccount"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceHost"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceDisplayName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.event.attribute.dynamic.serviceState"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount are objects with the following properties: - // - 'string' - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount')) { - const accounts = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount'); - - if (accounts) { - if (!Array.isArray(accounts)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount" is not an array'); - configFileCorrect = false; - } else { - accounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.sendToAccount"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceHost"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceDisplayName"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.destination.log.attribute.dynamic.serviceState"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header')) { - const headers = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static')) { - const attributes = config.get('Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.incidentTool.newRelic.serviceMonitor.sharedSettings.attribute.static"', - ); - configFileCorrect = false; - } - - // Butler.webhookNotification - if (!config.has('Butler.webhookNotification.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.enable"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.reloadTaskFailure - if (!config.has('Butler.webhookNotification.reloadTaskFailure.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.reloadTaskFailure.webhooks array are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.reloadTaskFailure.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.reloadTaskFailure.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskFailure.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskFailure.webhooks"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskAborted.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.reloadTaskAborted.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.reloadTaskAborted.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.reloadTaskAborted.webhooks'); - // If there is one or more entries in Butler.webhookNotification.reloadTaskAborted.webhooks, verify that they are objects with the correct properties - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.reloadTaskAborted.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.reloadTaskAborted.webhooks"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.serviceMonitor.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.rateLimit"'); - configFileCorrect = false; - } - - if (!config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.serviceMonitor.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.serviceMonitor.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.serviceMonitor.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}]" must be an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.serviceMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.serviceMonitor.webhooks"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.qlikSenseServerLicenseMonitor - if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseMonitor.webhooks"'); - configFileCorrect = false; - } - - // Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert - if (!config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.rateLimit"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks are objects with the following properties: - // { - // "description": "A description of the webhook", - // "webhookURL": "https://webhook.site/...", - // "httpMethod": "POST", - // "cert": { - // "enable": true, - // "rejectUnauthorized": true, - // "certCA": "/path/to/ca-cert.pem", - // }, - // } - if (config.has('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks')) { - const webhooks = config.get('Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks'); - - if (webhooks) { - if (!Array.isArray(webhooks)) { - logger.error('ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks" must be an array'); - configFileCorrect = false; - } else { - webhooks.forEach((webhook, index) => { - if (typeof webhook !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook, 'description')) { - logger.error( - `ASSERT CONFIG: Missing property "description" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'webhookURL')) { - logger.error( - `ASSERT CONFIG: Missing property "webhookURL" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'httpMethod')) { - logger.error( - `ASSERT CONFIG: Missing property "httpMethod" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook, 'cert')) { - logger.error( - `ASSERT CONFIG: Missing property "cert" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof webhook.cert !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'enable')) { - logger.error( - `ASSERT CONFIG: Missing property "enable" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'rejectUnauthorized')) { - logger.error( - `ASSERT CONFIG: Missing property "rejectUnauthorized" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - if (!Object.prototype.hasOwnProperty.call(webhook.cert, 'certCA')) { - logger.error( - `ASSERT CONFIG: Missing property "certCA" in "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks[${index}].cert"`, - ); - configFileCorrect = false; - } - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.webhookNotification.qlikSenseServerLicenseExpiryAlert.webhooks"'); - configFileCorrect = false; - } - - // Butler.scheduler - if (!config.has('Butler.scheduler.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.scheduler.configfile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.scheduler.configfile"'); - configFileCorrect = false; - } - - // Butler.mqttConfig - if (!config.has('Butler.mqttConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.brokerHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.brokerPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.brokerPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientId')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientId"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientCertFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientCertFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.azureEventGrid.clientKeyFile')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.azureEventGrid.clientKeyFile"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureSendFull')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureSendFull"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedSendFull')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedSendFull"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.subscriptionRootTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.subscriptionRootTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskStartTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskStartTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskFailureFullTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskFailureFullTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.taskAbortedFullTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.taskAbortedFullTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceRunningTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceRunningTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceStoppedTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStoppedTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.serviceStatusTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.serviceStatusTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseTopic"'); - configFileCorrect = false; - } - - if (!config.has('Butler.mqttConfig.qlikSenseServerLicenseExpireTopic')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.mqttConfig.qlikSenseServerLicenseExpireTopic"'); - configFileCorrect = false; - } - - // QS Cloud settings - if (!config.has('Butler.qlikSenseCloud.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.id')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.id"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.tenantUrl"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.authType')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.authType"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.auth.jwt.token"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.qmc"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.qlikSenseUrls.hub"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.comment')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.comment"'); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicContentOnly"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.webhookURL"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.messageType"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.basicMsgTemplate"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicContentOnly"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.webhookURL"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.messageType"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.basicMsgTemplate"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.fromUser"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.iconEmoji"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"', - ); - configFileCorrect = false; - } - - if ( - !config.has( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll', - ) - ) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.includeAll"', - ); - configFileCorrect = false; - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties: - // - email: 'string' - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) { - const users = config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user', - ); - - if (users) { - if (!Array.isArray(users)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user" must be an array', - ); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'email')) { - logger.error( - `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - } - }); - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user"', - ); - configFileCorrect = false; - } - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties: - // - email: 'string' - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) { - const users = config.get( - 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user', - ); - - if (users) { - if (!Array.isArray(users)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user" must be an array', - ); - configFileCorrect = false; - } else { - users.forEach((user, index) => { - if (typeof user !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]" must be an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(user, 'email')) { - logger.error( - `ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`, - ); - configFileCorrect = false; - } - } - }); - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user"', - ); - configFileCorrect = false; - } - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.headScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.tailScriptLogLines"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.priority"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress')) { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress"', - ); - configFileCorrect = false; - } - - // Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings - // It is ok for the array to be empty - if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) { - const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients'); - - if (recipients) { - if (!Array.isArray(recipients)) { - logger.error( - 'ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" must be an array', - ); - configFileCorrect = false; - } else { - recipients.forEach((recipient, index) => { - if (typeof recipient !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients[${index}]" must be a string`, - ); - configFileCorrect = false; - } - }); - } - } else if (recipients === null) { - logger.warn( - 'ASSERT CONFIG: No recipients defined for Qlik Sense cloud alert emails, "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" is empty.', - ); - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', - ); - configFileCorrect = false; - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"', - ); - configFileCorrect = false; - } - - // Butler.udpServerConfig - if (!config.has('Butler.udpServerConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.udpServerConfig.serverHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.serverHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.udpServerConfig.portTaskFailure')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.udpServerConfig.portTaskFailure"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.serverHost')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverHost"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.serverPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.serverPort"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerConfig.backgroundServerPort')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerConfig.backgroundServerPort"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileCopyApprovedDirectories are objects with the following properties: - // { - // fromDirectory: 'string', - // toDirectory: 'string' - // } - if (config.has('Butler.fileCopyApprovedDirectories')) { - const directories = config.get('Butler.fileCopyApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileCopyApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.fileCopyApprovedDirectories[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { - logger.error( - `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof directory.fromDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "fromDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { - logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]"`); - configFileCorrect = false; - } else if (typeof directory.toDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "toDirectory" property in "Butler.fileCopyApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileCopyApprovedDirectories"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileMoveApprovedDirectories are objects with the following properties: - // { - // fromDirectory: 'string', - // toDirectory: 'string' - // } - if (config.has('Butler.fileMoveApprovedDirectories')) { - const directories = config.get('Butler.fileMoveApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileMoveApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.fileMoveApprovedDirectories[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(directory, 'fromDirectory')) { - logger.error( - `ASSERT CONFIG: Missing "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof directory.fromDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "fromDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(directory, 'toDirectory')) { - logger.error(`ASSERT CONFIG: Missing "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]"`); - configFileCorrect = false; - } else if (typeof directory.toDirectory !== 'string') { - logger.error( - `ASSERT CONFIG: "toDirectory" property in "Butler.fileMoveApprovedDirectories[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileMoveApprovedDirectories"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.fileDeleteApprovedDirectories are objects with the following properties: - // - name: 'string' - if (config.has('Butler.fileDeleteApprovedDirectories')) { - const directories = config.get('Butler.fileDeleteApprovedDirectories'); - - if (directories) { - if (!Array.isArray(directories)) { - logger.error('ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories" is not an array'); - configFileCorrect = false; - } else { - directories.forEach((directory, index) => { - if (typeof directory !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.fileDeleteApprovedDirectories[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.fileDeleteApprovedDirectories"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerApiDocGenerate')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerApiDocGenerate"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.apiListEnbledEndpoints')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.apiListEnbledEndpoints"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.base62ToBase16')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base62ToBase16"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.base16ToBase62')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.base16ToBase62"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.butlerping')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.butlerping"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.createDir')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDir"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.createDirQVD')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.createDirQVD"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileDelete')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileDelete"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileMove')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileMove"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.fileCopy')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.fileCopy"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.keyValueStore')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.keyValueStore"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.mqttPublishMessage')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.mqttPublishMessage"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.createNewSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.createNewSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.getSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.getScheduleStatusAll"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.updateSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.updateSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.deleteSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.deleteSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.startSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.startSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.scheduler.stopSchedule')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.scheduler.stopSchedule"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseAppReload')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppReload"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseAppDump')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseAppDump"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseListApps')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseListApps"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.senseStartTask')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.senseStartTask"'); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsEnable.slackPostMessage')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsEnable.slackPostMessage"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount are objects with the following properties: - // - name: 'string' - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount')) { - const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount'); - - if (destinationAccounts) { - if (!Array.isArray(destinationAccounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount" is not an array', - ); - configFileCorrect = false; - } else { - destinationAccounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.destinationAccount"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) { - const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) { - const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static"', - ); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount are objects with the following properties: - // - name: 'string' - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount')) { - const destinationAccounts = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount'); - - if (destinationAccounts) { - if (!Array.isArray(destinationAccounts)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount" is not an array', - ); - configFileCorrect = false; - } else { - destinationAccounts.forEach((account, index) => { - if (typeof account !== 'string') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount[${index}]" is not a string`, - ); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header')) { - const headers = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header'); - - if (headers) { - if (!Array.isArray(headers)) { - logger.error('ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header" is not an array'); - configFileCorrect = false; - } else { - headers.forEach((header, index) => { - if (typeof header !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(header, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(header, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof header.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static')) { - const attributes = config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static'); - - if (attributes) { - if (!Array.isArray(attributes)) { - logger.error( - 'ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static" is not an array', - ); - configFileCorrect = false; - } else { - attributes.forEach((attribute, index) => { - if (typeof attribute !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(attribute, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(attribute, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof attribute.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error( - 'ASSERT CONFIG: Missing config file entry "Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static"', - ); - configFileCorrect = false; - } - - if (!config.has('Butler.startTaskFilter.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.enable"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.taskId are objects with the following properties: - // - name: 'string' - if (config.has('Butler.startTaskFilter.allowTask.taskId')) { - const taskIds = config.get('Butler.startTaskFilter.allowTask.taskId'); - - if (taskIds) { - if (!Array.isArray(taskIds)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId" is not an array'); - configFileCorrect = false; - } else { - taskIds.forEach((taskId, index) => { - if (typeof taskId !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.taskId[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.taskId"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.tag are objects with the following properties: - // - name: 'string' - if (config.has('Butler.startTaskFilter.allowTask.tag')) { - const tags = config.get('Butler.startTaskFilter.allowTask.tag'); - - if (tags) { - if (!Array.isArray(tags)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag" is not an array'); - configFileCorrect = false; - } else { - tags.forEach((tag, index) => { - if (typeof tag !== 'string') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.tag[${index}]" is not a string`); - configFileCorrect = false; - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.tag"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.startTaskFilter.allowTask.customProperty are objects with the following properties: - // { - // name: 'string', - // value: 'string' - // } - if (config.has('Butler.startTaskFilter.allowTask.customProperty')) { - const customProperties = config.get('Butler.startTaskFilter.allowTask.customProperty'); - - if (customProperties) { - if (!Array.isArray(customProperties)) { - logger.error('ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty" is not an array'); - configFileCorrect = false; - } else { - customProperties.forEach((customProperty, index) => { - if (typeof customProperty !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(customProperty, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof customProperty.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(customProperty, 'value')) { - logger.error( - `ASSERT CONFIG: Missing "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]"`, - ); - configFileCorrect = false; - } else if (typeof customProperty.value !== 'string') { - logger.error( - `ASSERT CONFIG: "value" property in "Butler.startTaskFilter.allowTask.customProperty[${index}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.startTaskFilter.allowTask.customProperty"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.frequency')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.frequency"'); - configFileCorrect = false; - } - - // Make sure all entries in Butler.serviceMonitor.monitor are objects with the following properties: - // { - // host: 'string', - // services: { - // name: 'string', - // friendlyName: 'string', - // } - // } - if (config.has('Butler.serviceMonitor.monitor')) { - const monitors = config.get('Butler.serviceMonitor.monitor'); - - if (monitors) { - if (!Array.isArray(monitors)) { - logger.error('ASSERT CONFIG: "Butler.serviceMonitor.monitor" is not an array'); - configFileCorrect = false; - } else { - monitors.forEach((monitor, index) => { - if (typeof monitor !== 'object') { - logger.error(`ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}]" is not an object`); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(monitor, 'host')) { - logger.error(`ASSERT CONFIG: Missing "host" property in "Butler.serviceMonitor.monitor[${index}]"`); - configFileCorrect = false; - } else if (typeof monitor.host !== 'string') { - logger.error(`ASSERT CONFIG: "host" property in "Butler.serviceMonitor.monitor[${index}]" is not a string`); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(monitor, 'services')) { - logger.error(`ASSERT CONFIG: Missing "services" property in "Butler.serviceMonitor.monitor[${index}]"`); - configFileCorrect = false; - } else if (!Array.isArray(monitor.services)) { - logger.error(`ASSERT CONFIG: "services" property in "Butler.serviceMonitor.monitor[${index}]" is not an array`); - configFileCorrect = false; - } else { - monitor.services.forEach((service, serviceIndex) => { - if (typeof service !== 'object') { - logger.error( - `ASSERT CONFIG: "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not an object`, - ); - configFileCorrect = false; - } else { - if (!Object.prototype.hasOwnProperty.call(service, 'name')) { - logger.error( - `ASSERT CONFIG: Missing "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, - ); - configFileCorrect = false; - } else if (typeof service.name !== 'string') { - logger.error( - `ASSERT CONFIG: "name" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, - ); - configFileCorrect = false; - } - - if (!Object.prototype.hasOwnProperty.call(service, 'friendlyName')) { - logger.error( - `ASSERT CONFIG: Missing "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]"`, - ); - configFileCorrect = false; - } else if (typeof service.friendlyName !== 'string') { - logger.error( - `ASSERT CONFIG: "friendlyName" property in "Butler.serviceMonitor.monitor[${index}].services[${serviceIndex}]" is not a string`, - ); - configFileCorrect = false; - } - } - }); - } - } - }); - } - } - } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.monitor"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.influxDb.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.influxDb.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.newRelic.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.newRelic.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.email.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.email.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.mqtt.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.mqtt.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.teams.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.teams.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.slack.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.slack.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.serviceMonitor.alertDestination.webhook.enable')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.serviceMonitor.alertDestination.webhook.enable"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCert')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCert"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCertKey')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertKey"'); - configFileCorrect = false; - } - - if (!config.has('Butler.cert.clientCertCA')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.cert.clientCertCA"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.engineVersion')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.engineVersion"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.useSSL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.useSSL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.headers.X-Qlik-User')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.headers.X-Qlik-User"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configEngine.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configEngine.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.authentication')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.authentication"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.host')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.host"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.port')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.port"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.useSSL')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.useSSL"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.headerKey')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerKey"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.headerValue')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.headerValue"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configQRS.rejectUnauthorized')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configQRS.rejectUnauthorized"'); - configFileCorrect = false; - } - - if (!config.has('Butler.configDirectories.qvdPath')) { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.configDirectories.qvdPath"'); - configFileCorrect = false; - } - - return configFileCorrect; -}; From b3a9a40a9f581d7e4df75e0acf1fcd4df0290b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 08:15:08 +0000 Subject: [PATCH 10/14] refactor(config): Remove support for "enabled" properties in config file, only support "enable". Implements #1243 --- src/app.js | 10 ++-------- src/butler.js | 5 +---- src/lib/telemetry.js | 21 +++++---------------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/app.js b/src/app.js index 620973eb..4a187f0e 100644 --- a/src/app.js +++ b/src/app.js @@ -39,10 +39,7 @@ async function build(opts = {}) { globals.initInfluxDB(); // Start the uptime monitor if it is enabled in the config - if ( - (globals.config.has('Butler.uptimeMonitor.enabled') && globals.config.get('Butler.uptimeMonitor.enabled') === true) || - (globals.config.has('Butler.uptimeMonitor.enable') && globals.config.get('Butler.uptimeMonitor.enable') === true) - ) { + if (globals.config.has('Butler.uptimeMonitor.enable') && globals.config.get('Butler.uptimeMonitor.enable') === true) { serviceUptimeStart(); } @@ -63,10 +60,7 @@ async function build(opts = {}) { globals.logger.verbose(`MAIN: Using client cert CA file: ${caFile}`); // Set up heartbeats, if enabled in the config file - if ( - (globals.config.has('Butler.heartbeat.enabled') && globals.config.get('Butler.heartbeat.enabled') === true) || - (globals.config.has('Butler.heartbeat.enable') && globals.config.get('Butler.heartbeat.enable') === true) - ) { + if (globals.config.has('Butler.heartbeat.enable') && globals.config.get('Butler.heartbeat.enable') === true) { setupHeartbeatTimer(globals.config, globals.logger); } diff --git a/src/butler.js b/src/butler.js index 0542c06c..28deb8a9 100644 --- a/src/butler.js +++ b/src/butler.js @@ -170,10 +170,7 @@ const start = async () => { } // Start Docker healthcheck REST server on port set in config file - if ( - (globals.config.has('Butler.dockerHealthCheck.enabled') && globals.config.get('Butler.dockerHealthCheck.enabled') === true) || - (globals.config.has('Butler.dockerHealthCheck.enable') && globals.config.get('Butler.dockerHealthCheck.enable') === true) - ) { + if (globals.config.has('Butler.dockerHealthCheck.enable') && globals.config.get('Butler.dockerHealthCheck.enable') === true) { try { globals.logger.verbose('MAIN: Starting Docker healthcheck server...'); diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index 5c4681e4..9990693d 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -70,32 +70,21 @@ const callRemoteURL = async () => { let restServer = 'null'; // Gather info on what features are enabled/disabled - if ( - (globals.config.has('Butler.heartbeat.enabled') && globals.config.get('Butler.heartbeat.enabled') === true) || - (globals.config.has('Butler.heartbeat.enable') && globals.config.get('Butler.heartbeat.enable') === true) - ) { + if (globals.config.has('Butler.heartbeat.enable') && globals.config.get('Butler.heartbeat.enable') === true) { heartbeat = true; } - if ( - (globals.config.has('Butler.dockerHealthCheck.enabled') && globals.config.get('Butler.dockerHealthCheck.enabled') === true) || - (globals.config.has('Butler.dockerHealthCheck.enable') && globals.config.get('Butler.dockerHealthCheck.enable') === true) - ) { + if (globals.config.has('Butler.dockerHealthCheck.enable') && globals.config.get('Butler.dockerHealthCheck.enable') === true) { dockerHealthCheck = true; } - if ( - (globals.config.has('Butler.uptimeMonitor.enabled') && globals.config.get('Butler.uptimeMonitor.enabled') === true) || - (globals.config.has('Butler.uptimeMonitor.enable') && globals.config.get('Butler.uptimeMonitor.enable') === true) - ) { + if (globals.config.has('Butler.uptimeMonitor.enable') && globals.config.get('Butler.uptimeMonitor.enable') === true) { uptimeMonitor = true; } if ( - (globals.config.has('Butler.uptimeMonitor.storeInInfluxdb.enabled') && - globals.config.get('Butler.uptimeMonitor.storeInInfluxdb.enabled') === true) || - (globals.config.has('Butler.uptimeMonitor.storeInInfluxdb.enable') && - globals.config.get('Butler.uptimeMonitor.storeInInfluxdb.enable') === true) + globals.config.has('Butler.uptimeMonitor.storeInInfluxdb.enable') && + globals.config.get('Butler.uptimeMonitor.storeInInfluxdb.enable') === true ) { uptimeMonitorStoreInInfluxdb = true; } From 20c95086892a5848fc7302dfb128999f780bdff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 08:29:32 +0000 Subject: [PATCH 11/14] Add QS Cloud enabled/disabled to anon telemetry data --- src/lib/telemetry.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index 9990693d..14ab417f 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -62,6 +62,11 @@ const callRemoteURL = async () => { let newRelicNotification_reloadTaskFailure = 'null'; let newRelicNotification_reloadTaskAborted = 'null'; + let qlikSenseCloud = 'null'; + let qlikSenseCloudReloadAppFailureTeamsNotification = 'null'; + let qlikSenseCloudReloadAppFailureSlackNotification = 'null'; + let qlikSenseCloudReloadAppFailureEmailNotification = 'null'; + let scheduler = 'null'; let mqtt = 'null'; let serviceMonitor = 'null'; @@ -294,6 +299,28 @@ const callRemoteURL = async () => { restServer = globals.config.get('Butler.restServerConfig.enable'); } + if (globals.config.has('Butler.qlikSenseCloud.enable')) { + qlikSenseCloud = globals.config.get('Butler.qlikSenseCloud.enable'); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable')) { + qlikSenseCloudReloadAppFailureTeamsNotification = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.enable', + ); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable')) { + qlikSenseCloudReloadAppFailureSlackNotification = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.enable', + ); + } + + if (globals.config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable')) { + qlikSenseCloudReloadAppFailureEmailNotification = globals.config.get( + 'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.enable', + ); + } + // Build body that can be sent to PostHog const body = { distinctId: globals.hostInfo.id, @@ -367,6 +394,11 @@ const callRemoteURL = async () => { feature_webhookNotificationReloadTaskAborted: webhookNotification_reloadTaskAborted, feature_webhookNotificationServiceMonitor: webhookNotification_serviceMonitor, + feature_qliksensecloud: qlikSenseCloud, + feature_qliksensecloudReloadAppFailureTeamsNotification: qlikSenseCloudReloadAppFailureTeamsNotification, + feature_qliksensecloudReloadAppFailureSlackNotification: qlikSenseCloudReloadAppFailureSlackNotification, + feature_qliksensecloudReloadAppFailureEmailNotification: qlikSenseCloudReloadAppFailureEmailNotification, + feature_mqtt: mqtt, feature_scheduler: scheduler, feature_serviceMonitor: serviceMonitor, @@ -419,6 +451,13 @@ const callRemoteURL = async () => { webhookNotificationReloadTaskAborted: webhookNotification_reloadTaskAborted, webhookNotificationServiceMonitor: webhookNotification_serviceMonitor, + qlikSenseCloud: { + enabled: qlikSenseCloud, + reloadAppFailureTeamsNotification: qlikSenseCloudReloadAppFailureTeamsNotification, + reloadAppFailureSlackNotification: qlikSenseCloudReloadAppFailureSlackNotification, + reloadAppFailureEmailNotification: qlikSenseCloudReloadAppFailureEmailNotification, + }, + mqtt, scheduler, serviceMonitor, From 985b74999014055dded7b5fd49d91c2a38966d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 16:34:31 +0000 Subject: [PATCH 12/14] refactor(config)!: Get rid of old typos and outdated structure in config file Implements #1242 --- src/api/api.js | 2 +- src/butler.js | 11 +- src/config/config-gen-api-docs.yaml | 25 ++- src/config/production_template.yaml | 25 ++- src/globals.js | 29 +++- src/lib/assert/assert_config_file.js | 38 ++++ src/lib/assert/config-file-schema.js | 84 +++++++-- src/lib/post_to_influxdb.js | 201 ++++++++++++++++------ src/lib/qseow/scriptlog.js | 11 +- src/lib/service_monitor.js | 47 +---- src/lib/service_uptime.js | 12 +- src/lib/smtp.js | 14 +- src/lib/telemetry.js | 10 +- src/qrs_util/app_tag_util.js | 7 +- src/qrs_util/does_task_exist.js | 7 +- src/qrs_util/get_app_owner.js | 7 +- src/qrs_util/get_tasks.js | 7 +- src/qrs_util/sense_start_task.js | 7 +- src/qrs_util/task_cp_util.js | 14 +- src/qrs_util/task_tag_util.js | 7 +- src/routes/rest_server/api.js | 5 +- src/routes/rest_server/sense_app.js | 9 +- src/routes/rest_server/sense_app_dump.js | 7 +- src/routes/rest_server/sense_list_apps.js | 9 +- 24 files changed, 390 insertions(+), 205 deletions(-) diff --git a/src/api/api.js b/src/api/api.js index 75ec3257..1c7d6d98 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -10,7 +10,7 @@ const apiGetAPIEndpointsEnabled = { items: { type: 'string', }, - examples: [['activeUserCount', 'activeUsers', 'apiListEnbledEndpoints']], + examples: [['activeUserCount', 'activeUsers', 'apiListEnabledEndpoints']], }, }, }, diff --git a/src/butler.js b/src/butler.js index 28deb8a9..efa683a7 100644 --- a/src/butler.js +++ b/src/butler.js @@ -40,7 +40,7 @@ const start = async () => { const udpInitTaskErrorServer = (await import('./udp/udp_handlers.js')).default; const mqttInitHandlers = (await import('./lib/mqtt_handlers.js')).default; - const { configFileStructureAssert, configFileNewRelicAssert, configFileInfluxDbAssert } = await import( + const { configFileStructureAssert, configFileNewRelicAssert, configFileInfluxDbAssert, configFileQsAssert } = await import( './lib/assert/assert_config_file.js' ); @@ -75,6 +75,15 @@ const start = async () => { } else { globals.logger.info('MAIN: Config file contains required InfluxDb data - all good.'); } + + // Verify that QS specific config settings are valid + resAssert = await configFileQsAssert(globals.config, globals.logger); + if (resAssert === false) { + globals.logger.error('MAIN: Config file does not contain required Qlik Sense data. Exiting.'); + process.exit(1); + } else { + globals.logger.info('MAIN: Config file contains required Qlik Sense data - all good.'); + } } } diff --git a/src/config/config-gen-api-docs.yaml b/src/config/config-gen-api-docs.yaml index 0361ba68..6e4e8041 100644 --- a/src/config/config-gen-api-docs.yaml +++ b/src/config/config-gen-api-docs.yaml @@ -90,12 +90,15 @@ Butler: username: ... password: ... dbName: ... - instanceTag: ... # Tag that can be used to differentiate data from multiple Butler instances # Default retention policy that should be created in InfluxDB when Butler creates a new database there. # Any data older than retention policy threshold will be purged from InfluxDB. retentionPolicy: name: 10d duration: 10d + tag: + static: + # - name: butler_instance + # value: dev reloadTaskFailure: enable: true tailScriptLogLines: 20 @@ -356,7 +359,7 @@ Butler: subject: 'Qlik Sense reload aborted: "{{taskName}}"' bodyFileDirectory: ... htmlTemplateFile: aborted-reload - fromAdress: ... + fromAddress: ... recipients: # - ... reloadTaskFailure: @@ -391,7 +394,7 @@ Butler: subject: '❌ Qlik Sense reload failed: "{{taskName}}"' bodyFileDirectory: ... htmlTemplateFile: failed-reload - fromAdress: ... + fromAddress: ... recipients: # - ... serviceStopped: @@ -400,7 +403,7 @@ Butler: subject: '❌ Windows service stopped on host {{host}}: "{{serviceDisplayName}}"' bodyFileDirectory: ... htmlTemplateFile: service-stopped - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: # - ... serviceStarted: @@ -409,7 +412,7 @@ Butler: subject: '✅ Windows service started on host {{host}}: "{{serviceDisplayName}}"' bodyFileDirectory: ... htmlTemplateFile: service-started - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: # - ... smtp: @@ -710,7 +713,7 @@ Butler: # Enable/disable individual REST API endpoints. Set config item below to true to enable that endpoint. restServerEndpointsEnable: - apiListEnbledEndpoints: true + apiListEnabledEndpoints: true base62ToBase16: true base16ToBase62: true butlerping: true @@ -912,7 +915,9 @@ Butler: port: 4747 useSSL: true headers: - X-Qlik-User: UserDirectory=Internal;UserId=sa_repository + static: + - name: X-Qlik-User # Header used to identify what user connection to QRS is made as + value: UserDirectory=Internal;UserId=sa_repository # What user connection to QRS is made as rejectUnauthorized: false # Set to false to ignore warnings/errors caused by Qlik Sense's self-signed certificates. # Set to true if the Qlik Sense root CA is available on the computer where Butler SOS is running. @@ -921,8 +926,10 @@ Butler: host: 10.11.12.13 useSSL: true port: 4242 - headerKey: X-Qlik-User # Header used to identify what user connection to QRS is made as - headerValue: UserDirectory=Internal; UserId=sa_repository # What user connection to QRS is made as + headers: + static: + - name: X-Qlik-User # Header used to identify what user connection to QRS is made as + value: UserDirectory=Internal;UserId=sa_repository # What user connection to QRS is made as rejectUnauthorized: false # Set to false to ignore warnings/errors caused by Qlik Sense's self-signed certificates. # Set to true if the Qlik Sense root CA is available on the computer where Butler SOS is running. diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index ba5adf5f..19423a61 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -107,12 +107,15 @@ Butler: username: user_joe password: joesecret dbName: butler # Name of database in InfluxDB to which Butler's data is written - instanceTag: DEV # Tag that can be used to differentiate data from multiple Butler instances # Default retention policy that should be created in InfluxDB when Butler creates a new database there. # Any data older than retention policy threshold will be purged from InfluxDB. retentionPolicy: name: 10d duration: 10d + tag: + static: # Static tags to attach to all data stored in InflixDB + # - name: butler_instance + # value: dev reloadTaskFailure: enable: true tailScriptLogLines: 20 @@ -374,7 +377,7 @@ Butler: subject: 'Qlik Sense reload aborted: "{{taskName}}"' # Email subject. Can use template fields bodyFileDirectory: path/to/email_templates # Directory where email body template files are stored htmlTemplateFile: aborted-reload # Name of email body template file to use - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: # Array of email addresses to which the notification email will be sent - - @@ -414,7 +417,7 @@ Butler: subject: 'Qlik Sense reload failed: "{{taskName}}"' # Email subject. Can use template fields bodyFileDirectory: path/to/email_templates # Directory where email body template files are stored htmlTemplateFile: failed-reload # Name of email body template file to use - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: # Array of email addresses to which the notification email will be sent - - @@ -424,7 +427,7 @@ Butler: subject: '❌ Windows service stopped on host {{host}}: "{{serviceDisplayName}}"' bodyFileDirectory: path/to/email_templates/email_templates htmlTemplateFile: service-stopped - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: - - @@ -434,7 +437,7 @@ Butler: subject: '✅ Windows service started on host {{host}}: "{{serviceDisplayName}}"' bodyFileDirectory: path/to/email_templates/email_templates htmlTemplateFile: service-started - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: - - @@ -1012,7 +1015,7 @@ Butler: subject: '❌ Qlik Sense reload failed: "{{taskName}}"' bodyFileDirectory: /path/to//email_templates htmlTemplateFile: failed-reload-qscloud - fromAdress: Qlik Sense (no-reply) + fromAddress: Qlik Sense (no-reply) recipients: # - emma@somecompany.com # - patrick@somecompany.com @@ -1033,7 +1036,9 @@ Butler: port: useSSL: true headers: - X-Qlik-User: UserDirectory=Internal;UserId=sa_repository + static: # http headers that are sent with every request to QRS. The "X-Qlik-User" is mandatory. + - name: X-Qlik-User # Header used to identify what user connection to QRS is made as + value: UserDirectory=Internal;UserId=sa_repository # What user connection to QRS is made as rejectUnauthorized: false # Set to false to ignore warnings/errors caused by Qlik Sense's self-signed certificates. # Set to true if the Qlik Sense root CA is available on the computer where Butler SOS is running. @@ -1042,8 +1047,10 @@ Butler: host: useSSL: true port: - headerKey: X-Qlik-User # Header used to identify what user connection to QRS is made as - headerValue: UserDirectory=Internal; UserId=sa_repository # What user connection to QRS is made as + headers: + static: # http headers that are sent with every request to QRS. The "X-Qlik-User" is mandatory. + - name: X-Qlik-User # Header used to identify what user connection to QRS is made as + value: UserDirectory=Internal;UserId=sa_repository # What user connection to QRS is made as rejectUnauthorized: false # Set to false to ignore warnings/errors caused by Qlik Sense's self-signed certificates. # Set to true if the Qlik Sense root CA is available on the computer where Butler SOS is running. diff --git a/src/globals.js b/src/globals.js index c8051312..c5035b58 100644 --- a/src/globals.js +++ b/src/globals.js @@ -290,8 +290,7 @@ class Settings { host: this.config.get('Butler.configQRS.host'), port: this.config.get('Butler.configQRS.port'), useSSL: this.config.get('Butler.configQRS.useSSL'), - headerKey: this.config.get('Butler.configQRS.headerKey'), - headerValue: this.config.get('Butler.configQRS.headerValue'), + headers: this.config.get('Butler.configQRS.headers'), rejectUnauthorized: this.config.get('Butler.configQRS.rejectUnauthorized'), cert: this.readCert(certPath), key: this.readCert(keyPath), @@ -698,6 +697,32 @@ class Settings { return false; } } + + // Function to get engine http headers, ready for use with axios + getEngineHttpHeaders() { + const headersConfig = this.configEngine.headers; + + // headers variable is an array of objects, each object has "name" and "value" properties + const headersObj = {}; + headersConfig.forEach((element) => { + headersObj[element.name] = element.value; + }); + + return headersObj; + } + + // Function to get QRS http headers, ready for use with axios + getQRSHttpHeaders() { + const headersConfig = this.configQRS.headers; + + // headers variable is an array of objects, each object has "name" and "value" properties + const headersObj = {}; + headersConfig.forEach((element) => { + headersObj[element.name] = element.value; + }); + + return headersObj; + } } export default new Settings(); diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js index dde0de9b..2a725ebd 100644 --- a/src/lib/assert/assert_config_file.js +++ b/src/lib/assert/assert_config_file.js @@ -7,6 +7,44 @@ import { getReloadTasksCustomProperties } from '../../qrs_util/task_cp_util.js'; import { confifgFileSchema } from './config-file-schema.js'; import globals from '../../globals.js'; +// Verify QS related settings in the config file +export const configFileQsAssert = async (config, configQRS, logger) => { + // The array Butler.configEngine.headers.static must contain + // - at least one object + // - one object with the property "X-Qlik-User" with a value on the format "UserDirectory=>; UserId=" + // + // The array consists of objets with the following properties: + // - name + // - value + + if (config.get('Butler.configEngine.headers.static').length === 0) { + logger.error('ASSERT CONFIG QS: Butler.configEngine.headers.static is an empty array in the config file. Aborting.'); + return false; + } + + if (config.get('Butler.configEngine.headers.static').filter((header) => header.name === 'X-Qlik-User').length === 0) { + logger.error( + 'ASSERT CONFIG QS: Butler.configEngine.headers.static does not contain an object with the property "X-Qlik-User" in the config file. Aborting.', + ); + return false; + } + + // Same check as above, but for the Butler.configQRS.headers.static array + if (config.get('Butler.configQRS.headers.static').length === 0) { + logger.error('ASSERT CONFIG QS: Butler.configQRS.headers.static is an empty array in the config file. Aborting.'); + return false; + } + + if (config.get('Butler.configQRS.headers.static').filter((header) => header.name === 'X-Qlik-User').length === 0) { + logger.error( + 'ASSERT CONFIG QS: Butler.configQRS.headers.static does not contain an object with the property "X-Qlik-User" in the config file. Aborting.', + ); + return false; + } + + return true; +}; + // Veriify InfluxDb related settings in the config file export const configFileInfluxDbAssert = async (config, configQRS, logger) => { // ------------------------------------------ diff --git a/src/lib/assert/config-file-schema.js b/src/lib/assert/config-file-schema.js index e08f80dd..a5ad8404 100755 --- a/src/lib/assert/config-file-schema.js +++ b/src/lib/assert/config-file-schema.js @@ -211,7 +211,6 @@ export const confifgFileSchema = { additionalProperties: false, }, dbName: { type: 'string' }, - instanceTag: { type: 'string' }, retentionPolicy: { type: 'object', properties: { @@ -221,6 +220,25 @@ export const confifgFileSchema = { required: ['name', 'duration'], additionalProperties: false, }, + tag: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, reloadTaskFailure: { type: 'object', properties: { @@ -315,8 +333,8 @@ export const confifgFileSchema = { 'hostPort', 'auth', 'dbName', - 'instanceTag', 'retentionPolicy', + 'tag', 'reloadTaskFailure', 'reloadTaskSuccess', ], @@ -1052,7 +1070,7 @@ export const confifgFileSchema = { subject: { type: 'string' }, bodyFileDirectory: { type: 'string' }, htmlTemplateFile: { type: 'string' }, - fromAdress: { type: 'string' }, + fromAddress: { type: 'string' }, recipients: { type: ['array', 'null'], items: { @@ -1072,7 +1090,7 @@ export const confifgFileSchema = { 'subject', 'bodyFileDirectory', 'htmlTemplateFile', - 'fromAdress', + 'fromAddress', 'recipients', ], additionalProperties: false, @@ -1157,7 +1175,7 @@ export const confifgFileSchema = { subject: { type: 'string' }, bodyFileDirectory: { type: 'string' }, htmlTemplateFile: { type: 'string' }, - fromAdress: { type: 'string' }, + fromAddress: { type: 'string' }, recipients: { type: ['array', 'null'], items: { @@ -1177,7 +1195,7 @@ export const confifgFileSchema = { 'subject', 'bodyFileDirectory', 'htmlTemplateFile', - 'fromAdress', + 'fromAddress', 'recipients', ], additionalProperties: false, @@ -1194,7 +1212,7 @@ export const confifgFileSchema = { subject: { type: 'string' }, bodyFileDirectory: { type: 'string' }, htmlTemplateFile: { type: 'string' }, - fromAdress: { type: 'string' }, + fromAddress: { type: 'string' }, recipients: { type: ['array', 'null'], items: { @@ -1208,7 +1226,7 @@ export const confifgFileSchema = { 'subject', 'bodyFileDirectory', 'htmlTemplateFile', - 'fromAdress', + 'fromAddress', 'recipients', ], additionalProperties: false, @@ -1225,7 +1243,7 @@ export const confifgFileSchema = { subject: { type: 'string' }, bodyFileDirectory: { type: 'string' }, htmlTemplateFile: { type: 'string' }, - fromAdress: { type: 'string' }, + fromAddress: { type: 'string' }, recipients: { type: ['array', 'null'], items: { @@ -1239,7 +1257,7 @@ export const confifgFileSchema = { 'subject', 'bodyFileDirectory', 'htmlTemplateFile', - 'fromAdress', + 'fromAddress', 'recipients', ], additionalProperties: false, @@ -2333,7 +2351,7 @@ export const confifgFileSchema = { restServerEndpointsEnable: { type: 'object', properties: { - apiListEnbledEndpoints: { type: 'boolean' }, + apiListEnabledEndpoints: { type: 'boolean' }, base62ToBase16: { type: 'boolean' }, base16ToBase62: { type: 'boolean' }, butlerping: { type: 'boolean' }, @@ -2382,7 +2400,7 @@ export const confifgFileSchema = { slackPostMessage: { type: 'boolean' }, }, required: [ - 'apiListEnbledEndpoints', + 'apiListEnabledEndpoints', 'base62ToBase16', 'base16ToBase62', 'butlerping', @@ -2949,9 +2967,22 @@ export const confifgFileSchema = { useSSL: { type: 'boolean' }, headers: { type: 'object', - properties: {}, - required: [], - additionalProperties: true, + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, }, rejectUnauthorized: { type: 'boolean' }, }, @@ -2969,11 +3000,28 @@ export const confifgFileSchema = { }, port: { type: 'number' }, useSSL: { type: 'boolean' }, - headerKey: { type: 'string' }, - headerValue: { type: 'string' }, + headers: { + type: 'object', + properties: { + static: { + type: ['array', 'null'], + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + }, + }, + required: ['static'], + additionalProperties: false, + }, rejectUnauthorized: { type: 'boolean' }, }, - required: ['authentication', 'host', 'useSSL', 'port', 'headerKey', 'headerValue', 'rejectUnauthorized'], + required: ['authentication', 'host', 'useSSL', 'port', 'headers', 'rejectUnauthorized'], additionalProperties: false, }, diff --git a/src/lib/post_to_influxdb.js b/src/lib/post_to_influxdb.js index 93b08529..74293023 100755 --- a/src/lib/post_to_influxdb.js +++ b/src/lib/post_to_influxdb.js @@ -5,13 +5,26 @@ export function postButlerMemoryUsageToInfluxdb(memory) { // Get Butler version const butlerVersion = globals.appVersion; + // Add version to tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + tags.version = butlerVersion; + let datapoint = [ { measurement: 'butler_memory_usage', - tags: { - butler_instance: memory.instanceTag, - version: butlerVersion, - }, + tags: tags, fields: { heap_used: memory.heapUsedMByte, heap_total: memory.heapTotalMByte, @@ -54,17 +67,25 @@ export function postButlerMemoryUsageToInfluxdb(memory) { export async function postQlikSenseVersionToInfluxDB(qlikSenseVersion) { globals.logger.verbose('INFLUXDB QLIK SENSE VERSION: Sending Qlik Sense version to InfluxDB'); - const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; - // Get tags from config file // Stored in array Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag const configTags = globals.config.get('Butler.qlikSenseVersion.versionMonitor.destination.influxDb.tag.static'); - const tags = { - butler_instance: instanceTag, - }; + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } - // Add tags from config file + // Add feature specific tags in configTags variable if (configTags) { // eslint-disable-next-line no-restricted-syntax for (const item of configTags) { @@ -114,17 +135,25 @@ export async function postQlikSenseVersionToInfluxDB(qlikSenseVersion) { export async function postQlikSenseServerLicenseStatusToInfluxDB(qlikSenseServerLicenseStatus) { globals.logger.verbose('INFLUXDB QLIK SENSE SERVER LICENSE STATUS: Sending Qlik Sense server license status to InfluxDB'); - const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; - // Get tags from config file // Stored in array Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag const configTags = globals.config.get('Butler.qlikSenseLicense.serverLicenseMonitor.destination.influxDb.tag.static'); - const tags = { - butler_instance: instanceTag, - }; + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); - // Add tags from config file + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + // Add feature specific tags in configTags variable if (configTags) { // eslint-disable-next-line no-restricted-syntax for (const item of configTags) { @@ -218,17 +247,25 @@ export async function postQlikSenseServerLicenseStatusToInfluxDB(qlikSenseServer export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatus) { globals.logger.verbose('INFLUXDB QLIK SENSE LICENSE STATUS: Sending Qlik Sense license status to InfluxDB'); - const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; - // Get tags from config file // Stored in array Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag const configTags = globals.config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static'); - const tags = { - butler_instance: instanceTag, - }; + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } - // Add tags from config file + // Add feature specific tags in configTags variable if (configTags) { // eslint-disable-next-line no-restricted-syntax for (const item of configTags) { @@ -370,19 +407,28 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu export async function postQlikSenseLicenseReleasedToInfluxDB(licenseInfo) { globals.logger.verbose('INFLUXDB QLIK SENSE LICENSE RELEASE: Sending info on released Qlik Sense license to InfluxDB'); - const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; - // Get tags from config file // Stored in array Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag const configTags = globals.config.get('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static'); - const tags = { - license_type: licenseInfo.licenseType, - user: `${licenseInfo.userDir}\\${licenseInfo.userId}`, - butler_instance: instanceTag, - }; + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); - // Add tags from config file + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + tags.license_type = licenseInfo.licenseType; + tags.user = `${licenseInfo.userDir}\\${licenseInfo.userId}`; + + // Add feature specific tags in configTags variable if (configTags) { // eslint-disable-next-line no-restricted-syntax for (const item of configTags) { @@ -439,16 +485,30 @@ export function postWindowsServiceStatusToInfluxDB(serviceStatus) { Disabled: 3, }; + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + // Add additional tags + tags.host = serviceStatus.host; + tags.service_name = serviceStatus.serviceName; + tags.display_name = serviceStatus.serviceDetails.displayName; + tags.friendly_name = serviceStatus.serviceFriendlyName; + let datapoint = [ { measurement: 'win_service_state', - tags: { - butler_instance: serviceStatus.instanceTag, - host: serviceStatus.host, - service_name: serviceStatus.serviceName, - display_name: serviceStatus.serviceDetails.displayName, - friendly_name: serviceStatus.serviceFriendlyName, - }, + tags: tags, fields: { state_num: serviceStateLookup[serviceStatus.serviceStatus] !== undefined ? serviceStateLookup[serviceStatus.serviceStatus] : -1, @@ -488,19 +548,34 @@ export function postReloadTaskSuccessNotificationInfluxDb(reloadParams) { try { globals.logger.verbose('INFLUXDB RELOAD TASK SUCCESS: Sending reload task notification to InfluxDB'); + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + // Add additional tags + tags.host = reloadParams.host; + tags.user = reloadParams.user; + tags.task_id = reloadParams.taskId; + tags.task_name = reloadParams.taskName; + tags.app_id = reloadParams.appId; + tags.app_name = reloadParams.appName; + tags.log_level = reloadParams.logLevel; + // Build InfluxDB datapoint let datapoint = [ { measurement: 'reload_task_success', - tags: { - host: reloadParams.host, - user: reloadParams.user, - task_id: reloadParams.taskId, - task_name: reloadParams.taskName, - app_id: reloadParams.appId, - app_name: reloadParams.appName, - log_level: reloadParams.logLevel, - }, + tags: tags, fields: { log_timestamp: reloadParams.logTimeStamp, execution_id: reloadParams.executionId, @@ -589,19 +664,35 @@ export function postReloadTaskSuccessNotificationInfluxDb(reloadParams) { export function postReloadTaskFailureNotificationInfluxDb(reloadParams) { try { globals.logger.info('INFLUXDB RELOAD TASK FAILED: Sending reload task notification to InfluxDB'); + + // Add tags + let tags = {}; + + // Get static tags as array from config file + const configStaticTags = globals.config.get('Butler.influxDb.tag.static'); + + // Add static tags to tags object + if (configStaticTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configStaticTags) { + tags[item.name] = item.value; + } + } + + // Add additional tags + tags.host = reloadParams.host; + tags.user = reloadParams.user; + tags.task_id = reloadParams.taskId; + tags.task_name = reloadParams.taskName; + tags.app_id = reloadParams.appId; + tags.app_name = reloadParams.appName; + tags.log_level = reloadParams.logLevel; + // Build InfluxDB datapoint let datapoint = [ { measurement: 'reload_task_failed', - tags: { - host: reloadParams.host, - user: reloadParams.user, - task_id: reloadParams.taskId, - task_name: reloadParams.taskName, - app_id: reloadParams.appId, - app_name: reloadParams.appName, - log_level: reloadParams.logLevel, - }, + tags: tags, fields: { log_timestamp: reloadParams.logTimeStamp, execution_id: reloadParams.executionId, diff --git a/src/lib/qseow/scriptlog.js b/src/lib/qseow/scriptlog.js index ac9b5a06..fb8bc55b 100644 --- a/src/lib/qseow/scriptlog.js +++ b/src/lib/qseow/scriptlog.js @@ -208,14 +208,17 @@ export async function getScriptLog(reloadTaskId, headLineCount, tailLineCount) { key: globals.configQRS.key, }); + // Get http headers from Butler config file + const httpHeaders = globals.getEngineHttpHeaders(); + + // Add x-qlik-xrfkey to headers + httpHeaders['x-qlik-xrfkey'] = 'abcdefghijklmnop'; + const axiosConfig = { url: `/qrs/download/reloadtask/${result2.body.value}/scriptlog.txt?xrfkey=abcdefghijklmnop`, method: 'get', baseURL: `https://${globals.configQRS.host}:${globals.configQRS.port}`, - headers: { - 'x-qlik-xrfkey': 'abcdefghijklmnop', - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, timeout: 10000, responseType: 'text', httpsAgent, diff --git a/src/lib/service_monitor.js b/src/lib/service_monitor.js index 1487e75f..e0a7e8ae 100644 --- a/src/lib/service_monitor.js +++ b/src/lib/service_monitor.js @@ -38,7 +38,6 @@ const serviceMonitorNewRelicSend1 = (config, logger, svc) => { const serviceMonitorMqttSend1 = (config, logger, svc) => { if (svc.serviceStatus === 'STOPPED') { if ( - !config.has('Butler.mqttConfig.serviceStoppedTopic') || config.get('Butler.mqttConfig.serviceStoppedTopic') === null || config.get('Butler.mqttConfig.serviceStoppedTopic').length === 0 ) { @@ -65,7 +64,6 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { } } else if (svc.serviceStatus === 'RUNNING') { if ( - !config.has('Butler.mqttConfig.serviceRunningTopic') || config.get('Butler.mqttConfig.serviceRunningTopic') === null || config.get('Butler.mqttConfig.serviceRunningTopic').length === 0 ) { @@ -94,11 +92,7 @@ const serviceMonitorMqttSend1 = (config, logger, svc) => { }; const serviceMonitorMqttSend2 = (config, logger, svc) => { - if ( - !config.has('Butler.mqttConfig.serviceStatusTopic') || - config.get('Butler.mqttConfig.serviceStatusTopic') === null || - config.get('Butler.mqttConfig.serviceStatusTopic').length === 0 - ) { + if (config.get('Butler.mqttConfig.serviceStatusTopic') === null || config.get('Butler.mqttConfig.serviceStatusTopic').length === 0) { logger.verbose(`"${svc.serviceName}"No MQTT topic defined in config entry "Butler.mqttConfig.serviceStatusTopic"`); } else { logger.verbose( @@ -231,9 +225,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // New Relic if ( - config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable') && config.get('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.stopped.enable') === true && - config.has('Butler.serviceMonitor.alertDestination.newRelic.enable') && config.get('Butler.serviceMonitor.alertDestination.newRelic.enable') === true ) { serviceMonitorNewRelicSend1(config, logger, { @@ -250,9 +242,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // MQTT if ( - config.has('Butler.mqttConfig.enable') && config.get('Butler.mqttConfig.enable') === true && - config.has('Butler.serviceMonitor.alertDestination.mqtt.enable') && config.get('Butler.serviceMonitor.alertDestination.mqtt.enable') === true ) { serviceMonitorMqttSend1(config, logger, { @@ -268,10 +258,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { } // Outgoing webhooks - if ( - globals.config.has('Butler.serviceMonitor.alertDestination.webhook.enable') && - globals.config.get('Butler.serviceMonitor.alertDestination.webhook.enable') === true - ) { + if (globals.config.get('Butler.serviceMonitor.alertDestination.webhook.enable') === true) { sendServiceMonitorWebhook({ serviceName: service.name, serviceFriendlyName: service.friendlyName, @@ -286,8 +273,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Post to Slack if ( - globals.config.has('Butler.slackNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.slack.enable') && globals.config.get('Butler.slackNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.slack.enable') === true ) { @@ -305,8 +290,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Post to Teams if ( - globals.config.has('Butler.teamsNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.teams.enable') && globals.config.get('Butler.teamsNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.teams.enable') === true ) { @@ -324,8 +307,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Send email if ( - globals.config.has('Butler.emailNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.email.enable') && globals.config.get('Butler.emailNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.email.enable') === true ) { @@ -371,9 +352,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // New Relic if ( - config.has('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable') && config.get('Butler.incidentTool.newRelic.serviceMonitor.monitorServiceState.running.enable') === true && - config.has('Butler.serviceMonitor.alertDestination.newRelic.enable') && config.get('Butler.serviceMonitor.alertDestination.newRelic.enable') === true ) { serviceMonitorNewRelicSend1(config, logger, { @@ -390,9 +369,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // MQTT if ( - config.has('Butler.mqttConfig.enable') && config.get('Butler.mqttConfig.enable') === true && - config.has('Butler.serviceMonitor.alertDestination.mqtt.enable') && config.get('Butler.serviceMonitor.alertDestination.mqtt.enable') === true ) { serviceMonitorMqttSend1(config, logger, { @@ -408,10 +385,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { } // Outgoing webhooks - if ( - globals.config.has('Butler.serviceMonitor.alertDestination.webhook.enable') && - globals.config.get('Butler.serviceMonitor.alertDestination.webhook.enable') === true - ) { + if (globals.config.get('Butler.serviceMonitor.alertDestination.webhook.enable') === true) { sendServiceMonitorWebhook({ serviceName: service.name, serviceFriendlyName: service.friendlyName, @@ -426,8 +400,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Post to Slack if ( - globals.config.has('Butler.slackNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.slack.enable') && globals.config.get('Butler.slackNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.slack.enable') === true ) { @@ -445,8 +417,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Post to Teams if ( - globals.config.has('Butler.teamsNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.teams.enable') && globals.config.get('Butler.teamsNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.teams.enable') === true ) { @@ -464,8 +434,6 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Send email if ( - globals.config.has('Butler.emailNotification.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.email.enable') && globals.config.get('Butler.emailNotification.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.email.enable') === true ) { @@ -486,9 +454,7 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // Messages sent no matter what the service status is // MQTT if ( - config.has('Butler.mqttConfig.enable') && config.get('Butler.mqttConfig.enable') === true && - config.has('Butler.serviceMonitor.alertDestination.mqtt.enable') && config.get('Butler.serviceMonitor.alertDestination.mqtt.enable') === true ) { serviceMonitorMqttSend2(config, logger, { @@ -502,17 +468,10 @@ const checkServiceStatus = async (config, logger, isFirstCheck = false) => { // InfluDB if ( - globals.config.has('Butler.influxDb.enable') && - globals.config.has('Butler.serviceMonitor.alertDestination.influxDb.enable') && globals.config.get('Butler.influxDb.enable') === true && globals.config.get('Butler.serviceMonitor.alertDestination.influxDb.enable') === true ) { - const instanceTag = globals.config.has('Butler.influxDb.instanceTag') - ? globals.config.get('Butler.influxDb.instanceTag') - : ''; - postWindowsServiceStatusToInfluxDB({ - instanceTag, serviceName: service.name, serviceFriendlyName: service.friendlyName, serviceStatus, diff --git a/src/lib/service_uptime.js b/src/lib/service_uptime.js index e0a486d9..85bb3f89 100644 --- a/src/lib/service_uptime.js +++ b/src/lib/service_uptime.js @@ -66,18 +66,11 @@ function serviceUptimeStart() { ); // Store to Influxdb if enabled - const butlerMemoryInfluxTag = globals.config.has('Butler.influxDb.instanceTag') - ? globals.config.get('Butler.influxDb.instanceTag') - : ''; - if ( - globals.config.has('Butler.influxDb.enable') && globals.config.get('Butler.influxDb.enable') === true && - globals.config.has('Butler.uptimeMonitor.storeInInfluxdb.enable') && globals.config.get('Butler.uptimeMonitor.storeInInfluxdb.enable') === true ) { postButlerMemoryUsageToInfluxdb({ - instanceTag: butlerMemoryInfluxTag, heapUsedMByte, heapTotalMByte, externalMemoryMByte, @@ -86,10 +79,7 @@ function serviceUptimeStart() { } // Post uptime to New Relic if enabled - if ( - globals.config.has('Butler.uptimeMonitor.storeNewRelic.enable') && - globals.config.get('Butler.uptimeMonitor.storeNewRelic.enable') === true - ) { + if (globals.config.get('Butler.uptimeMonitor.storeNewRelic.enable') === true) { postButlerUptimeToNewRelic({ intervalMillisec, heapUsed, diff --git a/src/lib/smtp.js b/src/lib/smtp.js index 40ebedb0..4fdc54ff 100644 --- a/src/lib/smtp.js +++ b/src/lib/smtp.js @@ -88,7 +88,7 @@ function isEmailReloadFailedNotificationConfigOk() { !globals.config.has('Butler.emailNotification.reloadTaskFailure.subject') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.bodyFileDirectory') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.htmlTemplateFile') || - !globals.config.has('Butler.emailNotification.reloadTaskFailure.fromAdress') || + !globals.config.has('Butler.emailNotification.reloadTaskFailure.fromAddress') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.recipients') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty') || !globals.config.has('Butler.emailNotification.reloadTaskFailure.alertEnableByCustomProperty.enable') || @@ -128,7 +128,7 @@ function isEmailReloadAbortedNotificationConfigOk() { !globals.config.has('Butler.emailNotification.reloadTaskAborted.subject') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.bodyFileDirectory') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.htmlTemplateFile') || - !globals.config.has('Butler.emailNotification.reloadTaskAborted.fromAdress') || + !globals.config.has('Butler.emailNotification.reloadTaskAborted.fromAddress') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.recipients') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty') || !globals.config.has('Butler.emailNotification.reloadTaskAborted.alertEnableByCustomProperty.enable') || @@ -163,7 +163,7 @@ function isEmailServiceMonitorNotificationConfig() { !globals.config.has('Butler.emailNotification.serviceStopped.subject') || !globals.config.has('Butler.emailNotification.serviceStopped.bodyFileDirectory') || !globals.config.has('Butler.emailNotification.serviceStopped.htmlTemplateFile') || - !globals.config.has('Butler.emailNotification.serviceStopped.fromAdress') || + !globals.config.has('Butler.emailNotification.serviceStopped.fromAddress') || !globals.config.has('Butler.emailNotification.serviceStopped.recipients') ) { // Not enough info in config file @@ -590,7 +590,7 @@ export async function sendReloadTaskFailureNotificationEmail(reloadParams) { // Only send email if there is at least one recipient if (recipientEmailAddress.length > 0) { sendEmail( - globals.config.get('Butler.emailNotification.reloadTaskFailure.fromAdress'), + globals.config.get('Butler.emailNotification.reloadTaskFailure.fromAddress'), [recipientEmailAddress], globals.config.get('Butler.emailNotification.reloadTaskFailure.priority'), globals.config.get('Butler.emailNotification.reloadTaskFailure.subject'), @@ -863,7 +863,7 @@ export async function sendReloadTaskAbortedNotificationEmail(reloadParams) { // Only send email if there is at least one recipient if (recipientEmailAddress.length > 0) { sendEmail( - globals.config.get('Butler.emailNotification.reloadTaskAborted.fromAdress'), + globals.config.get('Butler.emailNotification.reloadTaskAborted.fromAddress'), [recipientEmailAddress], globals.config.get('Butler.emailNotification.reloadTaskAborted.priority'), globals.config.get('Butler.emailNotification.reloadTaskAborted.subject'), @@ -943,7 +943,7 @@ export async function sendServiceMonitorNotificationEmail(serviceParams) { if (recipientEmailAddress.length > 0) { if (serviceParams.serviceStatus === 'STOPPED') { sendEmail( - globals.config.get('Butler.emailNotification.serviceStopped.fromAdress'), + globals.config.get('Butler.emailNotification.serviceStopped.fromAddress'), [recipientEmailAddress], globals.config.get('Butler.emailNotification.serviceStopped.priority'), globals.config.get('Butler.emailNotification.serviceStopped.subject'), @@ -953,7 +953,7 @@ export async function sendServiceMonitorNotificationEmail(serviceParams) { ); } else if (serviceParams.serviceStatus === 'RUNNING') { sendEmail( - globals.config.get('Butler.emailNotification.serviceStarted.fromAdress'), + globals.config.get('Butler.emailNotification.serviceStarted.fromAddress'), [recipientEmailAddress], globals.config.get('Butler.emailNotification.serviceStarted.priority'), globals.config.get('Butler.emailNotification.serviceStarted.subject'), diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index 14ab417f..ae34d343 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -14,7 +14,7 @@ const callRemoteURL = async () => { let uptimeMonitorStoreInInfluxdb = 'null'; let uptimeMonitorStoreInNewRelic = 'null'; - let api_apiListEnbledEndpoints = 'null'; + let api_apiListEnabledEndpoints = 'null'; let api_base62ToBase16 = 'null'; let api_base16ToBase62 = 'null'; let api_butlerPing = 'null'; @@ -99,8 +99,8 @@ const callRemoteURL = async () => { uptimeMonitorStoreInNewRelic = globals.config.get('Butler.uptimeMonitor.storeNewRelic.enable'); } - if (globals.config.has('Butler.restServerEndpointsEnable.apiListEnbledEndpoints')) { - api_apiListEnbledEndpoints = globals.config.get('Butler.restServerEndpointsEnable.apiListEnbledEndpoints'); + if (globals.config.has('Butler.restServerEndpointsEnable.apiListEnabledEndpoints')) { + api_apiListEnabledEndpoints = globals.config.get('Butler.restServerEndpointsEnable.apiListEnabledEndpoints'); } if (globals.config.has('Butler.restServerEndpointsEnable.base62ToBase16')) { @@ -346,7 +346,7 @@ const callRemoteURL = async () => { feature_uptimeMonitoStoreInInfluxdb: uptimeMonitorStoreInInfluxdb, feature_uptimeMonitoStoreInNewRelic: uptimeMonitorStoreInNewRelic, - feature_apiListEnbledEndpoints: api_apiListEnbledEndpoints, + feature_apiListEnabledEndpoints: api_apiListEnabledEndpoints, feature_apiBase62ToBase16: api_base62ToBase16, feature_apiBase16ToBase62: api_base16ToBase62, feature_apiButlerPing: api_butlerPing, @@ -426,7 +426,7 @@ const callRemoteURL = async () => { uptimeMonitor_storeInInfluxdb: uptimeMonitorStoreInInfluxdb, uptimeMonitor_storeInNewRelic: uptimeMonitorStoreInNewRelic, - apiEnbledEndpoints: globals.config.has('Butler.restServerEndpointsEnable') + apiEnabledEndpoints: globals.config.has('Butler.restServerEndpointsEnable') ? globals.config.get('Butler.restServerEndpointsEnable') : {}, diff --git a/src/qrs_util/app_tag_util.js b/src/qrs_util/app_tag_util.js index 22f0c6bf..c093b872 100644 --- a/src/qrs_util/app_tag_util.js +++ b/src/qrs_util/app_tag_util.js @@ -11,12 +11,13 @@ async function getAppTags(appId) { globals.logger.debug(`GETAPPTAGS: Retrieving all tags of app ${appId}`); try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: path.resolve(globals.configQRS.certPaths.certPath), keyFile: path.resolve(globals.configQRS.certPaths.keyPath), diff --git a/src/qrs_util/does_task_exist.js b/src/qrs_util/does_task_exist.js index d2fe2581..c20be82f 100644 --- a/src/qrs_util/does_task_exist.js +++ b/src/qrs_util/does_task_exist.js @@ -5,12 +5,13 @@ import globals from '../globals.js'; const doesTaskExist = async (taskId) => { // eslint-disable-next-line no-unused-vars try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: globals.configQRS.certPaths.certPath, keyFile: globals.configQRS.certPaths.keyPath, diff --git a/src/qrs_util/get_app_owner.js b/src/qrs_util/get_app_owner.js index 1b7e17fe..5c40a024 100644 --- a/src/qrs_util/get_app_owner.js +++ b/src/qrs_util/get_app_owner.js @@ -4,12 +4,13 @@ import globals from '../globals.js'; // Function for getting info about owner of Qlik Sense apps const getAppOwner = async (appId) => { try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: globals.configQRS.certPaths.certPath, keyFile: globals.configQRS.certPaths.keyPath, diff --git a/src/qrs_util/get_tasks.js b/src/qrs_util/get_tasks.js index 6684e6e1..bb4ab758 100644 --- a/src/qrs_util/get_tasks.js +++ b/src/qrs_util/get_tasks.js @@ -5,12 +5,13 @@ import globals from '../globals.js'; // filter: { tag: 'abc', customProperty: { name: 'def', value: 'ghi' } } const getTasks = async (filter) => { try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: globals.configQRS.certPaths.certPath, keyFile: globals.configQRS.certPaths.keyPath, diff --git a/src/qrs_util/sense_start_task.js b/src/qrs_util/sense_start_task.js index ee7d853b..8abaa452 100644 --- a/src/qrs_util/sense_start_task.js +++ b/src/qrs_util/sense_start_task.js @@ -4,12 +4,13 @@ import globals from '../globals.js'; // Function for starting Sense task, given its task ID (as it appears in the QMC task list) async function senseStartTask(taskId) { try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: globals.configQRS.certPaths.certPath, keyFile: globals.configQRS.certPaths.keyPath, diff --git a/src/qrs_util/task_cp_util.js b/src/qrs_util/task_cp_util.js index c4c25a1e..a20ab6a6 100644 --- a/src/qrs_util/task_cp_util.js +++ b/src/qrs_util/task_cp_util.js @@ -15,12 +15,13 @@ export async function isCustomPropertyValueSet(taskId, cpName, cpValue, logger) localLogger.debug(`Checking if value "${cpValue}" is set for custom property "${cpName}"`); try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: path.resolve(globals.configQRS.certPaths.certPath), keyFile: path.resolve(globals.configQRS.certPaths.keyPath), @@ -65,12 +66,13 @@ export async function getTaskCustomPropertyValues(taskId, cpName) { globals.logger.debug(`GETTASKCPVALUE: Retrieving all values for custom property "${cpName}" of reload task ${taskId}`); try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: path.resolve(globals.configQRS.certPaths.certPath), keyFile: path.resolve(globals.configQRS.certPaths.keyPath), diff --git a/src/qrs_util/task_tag_util.js b/src/qrs_util/task_tag_util.js index 98b6f42d..8e6338d2 100644 --- a/src/qrs_util/task_tag_util.js +++ b/src/qrs_util/task_tag_util.js @@ -11,12 +11,13 @@ async function getTaskTags(taskId) { globals.logger.debug(`GETTASKTAGS: Retrieving all tags of reload task ${taskId}`); try { + // Get http headers from Butler config file + const httpHeaders = globals.getQRSHttpHeaders(); + const qrsInstance = new QrsInteract({ hostname: globals.configQRS.host, portNumber: globals.configQRS.port, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }, + headers: httpHeaders, certificates: { certFile: path.resolve(globals.configQRS.certPaths.certPath), keyFile: path.resolve(globals.configQRS.certPaths.keyPath), diff --git a/src/routes/rest_server/api.js b/src/routes/rest_server/api.js index cc814c67..feced658 100644 --- a/src/routes/rest_server/api.js +++ b/src/routes/rest_server/api.js @@ -20,10 +20,7 @@ async function handlerGetAPIEndpointsEnabled(request, reply) { // eslint-disable-next-line no-unused-vars export default async (fastify, options) => { - if ( - globals.config.has('Butler.restServerEndpointsEnable.apiListEnbledEndpoints') && - globals.config.get('Butler.restServerEndpointsEnable.apiListEnbledEndpoints') - ) { + if (globals.config.get('Butler.restServerEndpointsEnable.apiListEnabledEndpoints')) { globals.logger.debug('Registering REST endpoint GET /v4/configfile/endpointsenabled'); fastify.get('/v4/configfile/endpointsenabled', apiGetAPIEndpointsEnabled, handlerGetAPIEndpointsEnabled); diff --git a/src/routes/rest_server/sense_app.js b/src/routes/rest_server/sense_app.js index 65f9b005..c20b7d37 100644 --- a/src/routes/rest_server/sense_app.js +++ b/src/routes/rest_server/sense_app.js @@ -56,7 +56,10 @@ async function handlerPutAppReload(request, reply) { // Use data in request to start Qlik Sense app reload globals.logger.verbose(`APPRELOAD: Reloading app: ${request.params.appId}`); - // create a new session + // Get http headers from Butler config file + const httpHeaders = globals.getEngineHttpHeaders(); + + // Create a new session const configEnigma = { schema: qixSchema, url: SenseUtilities.buildUrl({ @@ -70,9 +73,7 @@ async function handlerPutAppReload(request, reply) { new WebSocket(url, { key: globals.configEngine.key, cert: globals.configEngine.cert, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal;UserId=sa_repository', - }, + headers: httpHeaders, rejectUnauthorized: globals.config.get('Butler.configEngine.rejectUnauthorized'), }), }; diff --git a/src/routes/rest_server/sense_app_dump.js b/src/routes/rest_server/sense_app_dump.js index 673bae99..00f90266 100644 --- a/src/routes/rest_server/sense_app_dump.js +++ b/src/routes/rest_server/sense_app_dump.js @@ -51,6 +51,9 @@ async function handlerGetSenseAppDump(request, reply) { } else { globals.logger.info(`APPDUMP: Dumping app: ${request.params.appId}`); + // Get http headers from Butler config file + const httpHeaders = globals.getEngineHttpHeaders(); + // create a new session // TODO Maybe should use https://github.com/qlik-oss/enigma.js/blob/master/docs/api.md#senseutilitiesbuildurlconfig ? const configEnigma = { @@ -60,9 +63,7 @@ async function handlerGetSenseAppDump(request, reply) { new WebSocket(url, { key: globals.configEngine.key, cert: globals.configEngine.cert, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal;UserId=sa_repository', - }, + headers: httpHeaders, rejectUnauthorized: globals.config.get('Butler.configEngine.rejectUnauthorized'), }), }; diff --git a/src/routes/rest_server/sense_list_apps.js b/src/routes/rest_server/sense_list_apps.js index c88d0c7b..2e59ab67 100644 --- a/src/routes/rest_server/sense_list_apps.js +++ b/src/routes/rest_server/sense_list_apps.js @@ -45,7 +45,10 @@ async function handlerGetSenseListApps(request, reply) { logRESTCall(request); - // create a new session + // Get http headers from Butler config file + const httpHeaders = globals.getEngineHttpHeaders(); + + // Create a new session const configEnigma = { schema: qixSchema, url: `wss://${globals.configEngine.host}:${globals.configEngine.port}`, @@ -53,9 +56,7 @@ async function handlerGetSenseListApps(request, reply) { new WebSocket(url, { key: globals.configEngine.key, cert: globals.configEngine.cert, - headers: { - 'X-Qlik-User': 'UserDirectory=Internal;UserId=sa_repository', - }, + headers: httpHeaders, rejectUnauthorized: globals.config.get('Butler.configEngine.rejectUnauthorized'), }), }; From 054faa60bbff347dd49e0953cfddd75ea0e067c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 17:00:10 +0000 Subject: [PATCH 13/14] fix: Test case "senseapp" now works again Fixes #1244 --- src/globals.js | 18 ++++++++++++------ src/routes/rest_server/sense_app.js | 2 +- src/routes/rest_server/sense_app_dump.js | 11 ++++++++--- src/routes/rest_server/sense_list_apps.js | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/globals.js b/src/globals.js index c5035b58..27970431 100644 --- a/src/globals.js +++ b/src/globals.js @@ -272,25 +272,31 @@ class Settings { this.configQRS = null; if (this.config.has('Butler.restServerApiDocGenerate') === false || this.config.get('Butler.restServerApiDocGenerate') === false) { this.logger.debug('CONFIG: API doc mode=off'); + // Deep copy of headers object + const httpHeadersEngine = JSON.parse(JSON.stringify(this.config.get('Butler.configEngine.headers'))); + // Engine config this.configEngine = { engineVersion: this.config.get('Butler.configEngine.engineVersion'), host: this.config.get('Butler.configEngine.host'), port: this.config.get('Butler.configEngine.port'), isSecure: this.config.get('Butler.configEngine.useSSL'), - headers: this.config.get('Butler.configEngine.headers'), + headers: httpHeadersEngine, cert: this.readCert(this.config.get('Butler.cert.clientCert')), key: this.readCert(this.config.get('Butler.cert.clientCertKey')), rejectUnauthorized: this.config.get('Butler.configEngine.rejectUnauthorized'), }; + // Deep copy of headers object + const httpHeadersQRS = JSON.parse(JSON.stringify(this.config.get('Butler.configQRS.headers'))); + // QRS config this.configQRS = { authentication: this.config.get('Butler.configQRS.authentication'), host: this.config.get('Butler.configQRS.host'), port: this.config.get('Butler.configQRS.port'), useSSL: this.config.get('Butler.configQRS.useSSL'), - headers: this.config.get('Butler.configQRS.headers'), + headers: httpHeadersQRS, rejectUnauthorized: this.config.get('Butler.configQRS.rejectUnauthorized'), cert: this.readCert(certPath), key: this.readCert(keyPath), @@ -698,9 +704,9 @@ class Settings { } } - // Function to get engine http headers, ready for use with axios + // Function to get static engine http headers, ready for use with axios getEngineHttpHeaders() { - const headersConfig = this.configEngine.headers; + const headersConfig = this.configEngine.headers.static; // headers variable is an array of objects, each object has "name" and "value" properties const headersObj = {}; @@ -711,9 +717,9 @@ class Settings { return headersObj; } - // Function to get QRS http headers, ready for use with axios + // Function to get static QRS http headers, ready for use with axios getQRSHttpHeaders() { - const headersConfig = this.configQRS.headers; + const headersConfig = this.configQRS.headers.static; // headers variable is an array of objects, each object has "name" and "value" properties const headersObj = {}; diff --git a/src/routes/rest_server/sense_app.js b/src/routes/rest_server/sense_app.js index c20b7d37..4743c7d7 100644 --- a/src/routes/rest_server/sense_app.js +++ b/src/routes/rest_server/sense_app.js @@ -40,7 +40,7 @@ async function handlerPutAppReload(request, reply) { b = upath.dirname(a); // Add path to package.json file - c = upath.join(b, '..', '..', schemaFile); + c = upath.join(b, '..', '..', '..', schemaFile); } globals.logger.verbose(`APPDUMP: Using engine schema in file: ${c}`); diff --git a/src/routes/rest_server/sense_app_dump.js b/src/routes/rest_server/sense_app_dump.js index 00f90266..d6fb23f6 100644 --- a/src/routes/rest_server/sense_app_dump.js +++ b/src/routes/rest_server/sense_app_dump.js @@ -9,6 +9,7 @@ import upath from 'upath'; import globals from '../../globals.js'; import { logRESTCall } from '../../lib/log_rest_call.js'; import { apiGetSenseAppDump, apiGetAppDump } from '../../api/sense_app_dump.js'; +import SenseUtilities from 'enigma.js/sense-utilities.js'; async function handlerGetSenseAppDump(request, reply) { try { @@ -37,7 +38,7 @@ async function handlerGetSenseAppDump(request, reply) { b = upath.dirname(a); // Add path to package.json file - c = upath.join(b, '..', '..', schemaFile); + c = upath.join(b, '..', '..', '..', schemaFile); } globals.logger.verbose(`APPDUMP: Using engine schema in file: ${c}`); @@ -55,10 +56,14 @@ async function handlerGetSenseAppDump(request, reply) { const httpHeaders = globals.getEngineHttpHeaders(); // create a new session - // TODO Maybe should use https://github.com/qlik-oss/enigma.js/blob/master/docs/api.md#senseutilitiesbuildurlconfig ? const configEnigma = { schema: qixSchema, - url: `wss://${globals.configEngine.host}:${globals.configEngine.port}`, + url: SenseUtilities.buildUrl({ + host: globals.configEngine.host, + port: globals.configEngine.port, + prefix: '', + secure: true, + }), createSocket: (url) => new WebSocket(url, { key: globals.configEngine.key, diff --git a/src/routes/rest_server/sense_list_apps.js b/src/routes/rest_server/sense_list_apps.js index 2e59ab67..a7a9382d 100644 --- a/src/routes/rest_server/sense_list_apps.js +++ b/src/routes/rest_server/sense_list_apps.js @@ -37,7 +37,7 @@ async function handlerGetSenseListApps(request, reply) { b = upath.dirname(a); // Add path to package.json file - c = upath.join(b, '..', '..', schemaFile); + c = upath.join(b, '..', '..', '..', schemaFile); } globals.logger.verbose(`APPDUMP: Using engine schema in file: ${c}`); From 0012f6d1cc80d73df102f0aff660d5f09b609769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 1 Oct 2024 18:09:20 +0000 Subject: [PATCH 14/14] fix(scheduler): Fix broken "scheduler" test case Fixes #1245 --- src/test/routes/scheduler.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/routes/scheduler.test.js b/src/test/routes/scheduler.test.js index e1763614..d646a4e2 100644 --- a/src/test/routes/scheduler.test.js +++ b/src/test/routes/scheduler.test.js @@ -404,6 +404,12 @@ describe('H8: GET /v4/schedules/status', () => { test('Response should be a string', () => { expect(result.data).toBeTruthy(); - expect(typeof result.data).toBe('string'); + + // Should either an empty object or a string + if (typeof result.data === 'object') { + expect(result.data).toEqual({}); + } else { + expect(typeof result.data).toBe('string'); + } }); });