"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SyntheticsService = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _rxjs = require("rxjs"); var _pMap = _interopRequireDefault(require("p-map")); var _common = require("@kbn/spaces-plugin/common"); var _constants = require("@kbn/spaces-plugin/common/constants"); var _clean_up_task = require("./private_location/clean_up_task"); var _saved_objects = require("../../common/types/saved_objects"); var _monitor_upgrade_sender = require("../routes/telemetry/monitor_upgrade_sender"); var _install_index_templates = require("../routes/synthetics_service/install_index_templates"); var _get_api_key = require("./get_api_key"); var _get_es_hosts = require("./get_es_hosts"); var _service_api_client = require("./service_api_client"); var _runtime_types = require("../../common/runtime_types"); var _get_service_locations = require("./get_service_locations"); var _secrets = require("./utils/secrets"); var _format_configs = require("./formatters/public_formatters/format_configs"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ /* eslint-disable max-classes-per-file */ const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE = 'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects'; const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID = 'UPTIME:SyntheticsService:sync-task'; const SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT = '5m'; class SyntheticsService { constructor(server) { var _server$config$servic; (0, _defineProperty2.default)(this, "logger", void 0); (0, _defineProperty2.default)(this, "esClient", void 0); (0, _defineProperty2.default)(this, "server", void 0); (0, _defineProperty2.default)(this, "apiClient", void 0); (0, _defineProperty2.default)(this, "config", void 0); (0, _defineProperty2.default)(this, "esHosts", void 0); (0, _defineProperty2.default)(this, "locations", void 0); (0, _defineProperty2.default)(this, "throttling", void 0); (0, _defineProperty2.default)(this, "indexTemplateExists", void 0); (0, _defineProperty2.default)(this, "indexTemplateInstalling", void 0); (0, _defineProperty2.default)(this, "isAllowed", void 0); (0, _defineProperty2.default)(this, "signupUrl", void 0); (0, _defineProperty2.default)(this, "syncErrors", []); (0, _defineProperty2.default)(this, "invalidApiKeyError", void 0); this.logger = server.logger; this.server = server; this.config = (_server$config$servic = server.config.service) !== null && _server$config$servic !== void 0 ? _server$config$servic : {}; this.isAllowed = false; this.signupUrl = null; this.apiClient = new _service_api_client.ServiceAPIClient(server.logger, this.config, this.server); this.esHosts = (0, _get_es_hosts.getEsHosts)({ config: this.config, cloud: server.cloud }); this.locations = []; } async setup(taskManager) { this.registerSyncTask(taskManager); (0, _clean_up_task.registerCleanUpTask)(taskManager, this.server); await this.registerServiceLocations(); const { allowed, signupUrl } = await this.apiClient.checkAccountAccessStatus(); this.isAllowed = allowed; this.signupUrl = signupUrl; } start(taskManager) { var _this$config; if ((_this$config = this.config) !== null && _this$config !== void 0 && _this$config.manifestUrl) { this.scheduleSyncTask(taskManager); } this.setupIndexTemplates(); } async setupIndexTemplates() { var _this$config2; if (process.env.CI && !((_this$config2 = this.config) !== null && _this$config2 !== void 0 && _this$config2.manifestUrl)) { // skip installation on CI return; } if (this.indexTemplateExists) { // if already installed, don't need to reinstall return; } try { if (!this.indexTemplateInstalling) { this.indexTemplateInstalling = true; const installedPackage = await (0, _install_index_templates.installSyntheticsIndexTemplates)(this.server); this.indexTemplateInstalling = false; if (installedPackage.name === 'synthetics' && installedPackage.install_status === 'installed') { this.logger.info('Installed synthetics index templates'); this.indexTemplateExists = true; } else if (installedPackage.name === 'synthetics' && installedPackage.install_status === 'install_failed') { this.logger.warn(new IndexTemplateInstallationError()); this.indexTemplateExists = false; } } } catch (e) { this.logger.error(e); this.indexTemplateInstalling = false; this.logger.warn(new IndexTemplateInstallationError()); } } async registerServiceLocations() { const service = this; try { const result = await (0, _get_service_locations.getServiceLocations)(service.server); service.throttling = result.throttling; service.locations = result.locations; service.apiClient.locations = result.locations; } catch (e) { this.logger.error(e); } } registerSyncTask(taskManager) { var _this$config$syncInte; const service = this; const interval = (_this$config$syncInte = this.config.syncInterval) !== null && _this$config$syncInte !== void 0 ? _this$config$syncInte : SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; taskManager.registerTaskDefinitions({ [SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE]: { title: 'Synthetics Service - Sync Saved Monitors', description: 'This task periodically pushes saved monitors to Synthetics Service.', timeout: '1m', maxAttempts: 3, createTaskRunner: ({ taskInstance }) => { return { // Perform the work of the task. The return value should fit the TaskResult interface. async run() { const { state } = taskInstance; try { await service.registerServiceLocations(); const { allowed, signupUrl } = await service.apiClient.checkAccountAccessStatus(); service.isAllowed = allowed; service.signupUrl = signupUrl; if (service.isAllowed && service.config.manifestUrl) { service.setupIndexTemplates(); await service.pushConfigs(); } } catch (e) { (0, _monitor_upgrade_sender.sendErrorTelemetryEvents)(service.logger, service.server.telemetry, { reason: 'Failed to run scheduled sync task', message: e === null || e === void 0 ? void 0 : e.message, type: 'runTaskError', code: e === null || e === void 0 ? void 0 : e.code, status: e.status, stackVersion: service.server.stackVersion }); service.logger.error(e); } return { state, schedule: { interval } }; }, async cancel() { var _service$logger; (_service$logger = service.logger) === null || _service$logger === void 0 ? void 0 : _service$logger.warn(`Task ${SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID} timed out`); } }; } } }); } async scheduleSyncTask(taskManager) { var _this$config$syncInte2; const interval = (_this$config$syncInte2 = this.config.syncInterval) !== null && _this$config$syncInte2 !== void 0 ? _this$config$syncInte2 : SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; try { var _this$logger, _taskInstance$schedul; const taskInstance = await taskManager.ensureScheduled({ id: SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID, taskType: SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE, schedule: { interval }, params: {}, state: {}, scope: ['uptime'] }); (_this$logger = this.logger) === null || _this$logger === void 0 ? void 0 : _this$logger.info(`Task ${SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID} scheduled with interval ${(_taskInstance$schedul = taskInstance.schedule) === null || _taskInstance$schedul === void 0 ? void 0 : _taskInstance$schedul.interval}.`); return taskInstance; } catch (e) { var _e$message, _this$logger2, _this$logger3; (0, _monitor_upgrade_sender.sendErrorTelemetryEvents)(this.logger, this.server.telemetry, { reason: 'Failed to schedule sync task', message: (_e$message = e === null || e === void 0 ? void 0 : e.message) !== null && _e$message !== void 0 ? _e$message : e, type: 'scheduleTaskError', code: e === null || e === void 0 ? void 0 : e.code, status: e.status, stackVersion: this.server.stackVersion }); (_this$logger2 = this.logger) === null || _this$logger2 === void 0 ? void 0 : _this$logger2.error(e); (_this$logger3 = this.logger) === null || _this$logger3 === void 0 ? void 0 : _this$logger3.error(`Error running synthetics syncs task: ${SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID}, ${e === null || e === void 0 ? void 0 : e.message}`); return null; } } async getLicense() { var _license, _license2; this.esClient = this.getESClient(); let license; if (this.esClient === undefined || this.esClient === null) { throw Error('Cannot sync monitors with the Synthetics service. Elasticsearch client is unavailable: cannot retrieve license information'); } try { var _await$this$esClient$; license = (_await$this$esClient$ = await this.esClient.license.get()) === null || _await$this$esClient$ === void 0 ? void 0 : _await$this$esClient$.license; } catch (e) { throw new Error(`Cannot sync monitors with the Synthetics service. Unable to determine license level: ${e}`); } if (((_license = license) === null || _license === void 0 ? void 0 : _license.status) === 'expired') { throw new Error('Cannot sync monitors with the Synthetics service. License is expired.'); } if (!((_license2 = license) !== null && _license2 !== void 0 && _license2.type)) { throw new Error('Cannot sync monitors with the Synthetics service. Unable to determine license level.'); } return license; } getESClient() { var _this$server$coreStar; if (!this.server.coreStart) { return; } return (_this$server$coreStar = this.server.coreStart) === null || _this$server$coreStar === void 0 ? void 0 : _this$server$coreStar.elasticsearch.client.asInternalUser; } async getOutput() { const { apiKey, isValid } = await (0, _get_api_key.getAPIKeyForSyntheticsService)({ server: this.server }); if (!isValid) { this.server.logger.error('API key is not valid. Cannot push monitor configuration to synthetics public testing locations'); this.invalidApiKeyError = true; return null; } return { hosts: this.esHosts, api_key: `${apiKey === null || apiKey === void 0 ? void 0 : apiKey.id}:${apiKey === null || apiKey === void 0 ? void 0 : apiKey.apiKey}` }; } async inspectConfig(config) { if (!config) { return null; } const monitors = this.formatConfigs(config); const license = await this.getLicense(); const output = await this.getOutput(); if (output) { return await this.apiClient.inspect({ monitors, output, license }); } return null; } async addConfigs(configs) { try { if (configs.length === 0) { return; } const monitors = this.formatConfigs(configs); const license = await this.getLicense(); const output = await this.getOutput(); if (output) { this.logger.debug(`1 monitor will be pushed to synthetics service.`); this.syncErrors = await this.apiClient.post({ monitors, output, license }); } return this.syncErrors; } catch (e) { this.logger.error(e); } } async editConfig(monitorConfig, isEdit = true) { try { if (monitorConfig.length === 0) { return; } const license = await this.getLicense(); const monitors = this.formatConfigs(monitorConfig); const output = await this.getOutput(); if (output) { const data = { monitors, output, isEdit, license }; this.syncErrors = await this.apiClient.put(data); } return this.syncErrors; } catch (e) { this.logger.error(e); } } async pushConfigs() { const license = await this.getLicense(); const service = this; const subject = new _rxjs.Subject(); let output = null; subject.subscribe(async monitors => { try { if (monitors.length === 0 || !this.config.manifestUrl) { return; } if (!output) { output = await this.getOutput(); if (!output) { (0, _monitor_upgrade_sender.sendErrorTelemetryEvents)(service.logger, service.server.telemetry, { reason: 'API key is not valid.', message: 'Failed to push configs. API key is not valid.', type: 'invalidApiKey', stackVersion: service.server.stackVersion }); return; } } this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); service.syncErrors = await this.apiClient.syncMonitors({ monitors, output, license }); } catch (e) { (0, _monitor_upgrade_sender.sendErrorTelemetryEvents)(service.logger, service.server.telemetry, { reason: 'Failed to push configs to service', message: e === null || e === void 0 ? void 0 : e.message, type: 'pushConfigsError', code: e === null || e === void 0 ? void 0 : e.code, status: e.status, stackVersion: service.server.stackVersion }); this.logger.error(e); } }); await this.getMonitorConfigs(subject); } async runOnceConfigs(configs) { if (!configs) { return; } const monitors = this.formatConfigs(configs); if (monitors.length === 0) { return; } const license = await this.getLicense(); const output = await this.getOutput(); if (!output) { return; } try { return await this.apiClient.runOnce({ monitors, output, license }); } catch (e) { this.logger.error(e); throw e; } } async deleteConfigs(configs) { try { if (configs.length === 0) { return; } const license = await this.getLicense(); const hasPublicLocations = configs.some(config => config.monitor.locations.some(({ isServiceManaged }) => isServiceManaged)); if (hasPublicLocations) { const output = await this.getOutput(); if (!output) { return; } const data = { output, monitors: this.formatConfigs(configs), license }; return await this.apiClient.delete(data); } } catch (e) { this.server.logger.error(e); } } async deleteAllConfigs() { const license = await this.getLicense(); const subject = new _rxjs.Subject(); subject.subscribe(async monitors => { const hasPublicLocations = monitors.some(config => config.locations.some(({ isServiceManaged }) => isServiceManaged)); if (hasPublicLocations) { const output = await this.getOutput(); if (!output) { return; } const data = { output, monitors, license }; return await this.apiClient.delete(data); } }); await this.getMonitorConfigs(subject); } async getMonitorConfigs(subject) { const soClient = this.server.savedObjectsClient; const encryptedClient = this.server.encryptedSavedObjects.getClient(); if (!(soClient !== null && soClient !== void 0 && soClient.find)) { return []; } const paramsBySpace = await this.getSyntheticsParams(); const finder = soClient.createPointInTimeFinder({ type: _saved_objects.syntheticsMonitorType, perPage: 100, namespaces: [_constants.ALL_SPACES_ID] }); for await (const result of finder.find()) { const monitors = await this.decryptMonitors(result.saved_objects, encryptedClient); const configDataList = (monitors !== null && monitors !== void 0 ? monitors : []).map(monitor => { var _monitor$namespaces$, _monitor$namespaces, _paramsBySpace$ALL_SP; const attributes = monitor.attributes; const monitorSpace = (_monitor$namespaces$ = (_monitor$namespaces = monitor.namespaces) === null || _monitor$namespaces === void 0 ? void 0 : _monitor$namespaces[0]) !== null && _monitor$namespaces$ !== void 0 ? _monitor$namespaces$ : _common.DEFAULT_SPACE_ID; const params = paramsBySpace[monitorSpace]; return { params: { ...params, ...((_paramsBySpace$ALL_SP = paramsBySpace === null || paramsBySpace === void 0 ? void 0 : paramsBySpace[_constants.ALL_SPACES_ID]) !== null && _paramsBySpace$ALL_SP !== void 0 ? _paramsBySpace$ALL_SP : {}) }, monitor: (0, _secrets.normalizeSecrets)(monitor).attributes, configId: monitor.id, heartbeatId: attributes[_runtime_types.ConfigKey.MONITOR_QUERY_ID] }; }); const formattedConfigs = this.formatConfigs(configDataList); subject.next(formattedConfigs); } await finder.close(); } async decryptMonitors(monitors, encryptedClient) { const start = performance.now(); const decryptedMonitors = await (0, _pMap.default)(monitors, monitor => new Promise(resolve => { var _monitor$namespaces2; encryptedClient.getDecryptedAsInternalUser(_saved_objects.syntheticsMonitorType, monitor.id, { namespace: (_monitor$namespaces2 = monitor.namespaces) === null || _monitor$namespaces2 === void 0 ? void 0 : _monitor$namespaces2[0] }).then(decryptedMonitor => resolve(decryptedMonitor)).catch(e => { this.logger.error(e); (0, _monitor_upgrade_sender.sendErrorTelemetryEvents)(this.logger, this.server.telemetry, { reason: 'Failed to decrypt monitor', message: e === null || e === void 0 ? void 0 : e.message, type: 'runTaskError', code: e === null || e === void 0 ? void 0 : e.code, status: e.status, stackVersion: this.server.stackVersion }); resolve(null); }); })); const end = performance.now(); const duration = end - start; this.logger.debug(`Decrypted ${monitors.length} monitors. Took ${duration} milliseconds`, { event: { duration }, monitors: monitors.length }); return decryptedMonitors.filter(monitor => monitor !== null); } async getSyntheticsParams({ spaceId, hideParams = false, canSave = true } = {}) { if (!canSave) { return Object.create(null); } const encryptedClient = this.server.encryptedSavedObjects.getClient(); const paramsBySpace = Object.create(null); const finder = await encryptedClient.createPointInTimeFinderDecryptedAsInternalUser({ type: _saved_objects.syntheticsParamType, perPage: 1000, namespaces: spaceId ? [spaceId] : [_constants.ALL_SPACES_ID] }); for await (const response of finder.find()) { response.saved_objects.forEach(param => { var _param$namespaces; (_param$namespaces = param.namespaces) === null || _param$namespaces === void 0 ? void 0 : _param$namespaces.forEach(namespace => { if (!paramsBySpace[namespace]) { paramsBySpace[namespace] = Object.create(null); } paramsBySpace[namespace][param.attributes.key] = hideParams ? '"*******"' : param.attributes.value; }); }); } // no need to wait here finder.close(); if (paramsBySpace[_constants.ALL_SPACES_ID]) { Object.keys(paramsBySpace).forEach(space => { if (space !== _constants.ALL_SPACES_ID) { paramsBySpace[space] = Object.assign(paramsBySpace[_constants.ALL_SPACES_ID], paramsBySpace[space]); } }); if (spaceId) { var _paramsBySpace$spaceI, _paramsBySpace$ALL_SP2; paramsBySpace[spaceId] = { ...((_paramsBySpace$spaceI = paramsBySpace === null || paramsBySpace === void 0 ? void 0 : paramsBySpace[spaceId]) !== null && _paramsBySpace$spaceI !== void 0 ? _paramsBySpace$spaceI : {}), ...((_paramsBySpace$ALL_SP2 = paramsBySpace === null || paramsBySpace === void 0 ? void 0 : paramsBySpace[_constants.ALL_SPACES_ID]) !== null && _paramsBySpace$ALL_SP2 !== void 0 ? _paramsBySpace$ALL_SP2 : {}) }; } } return paramsBySpace; } formatConfigs(configData) { const configDataList = Array.isArray(configData) ? configData : [configData]; return configDataList.map(config => { const { str: paramsString, params } = (0, _format_configs.mixParamsWithGlobalParams)(config.params, config.monitor); const asHeartbeatConfig = (0, _format_configs.formatHeartbeatRequest)(config, paramsString); return (0, _format_configs.formatMonitorConfigFields)(Object.keys(asHeartbeatConfig), asHeartbeatConfig, this.logger, params !== null && params !== void 0 ? params : {}); }); } } exports.SyntheticsService = SyntheticsService; class IndexTemplateInstallationError extends Error { constructor() { super(); this.message = 'Failed to install synthetics index templates.'; this.name = 'IndexTemplateInstallationError'; } }