"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.getActionStatuses = getActionStatuses; exports.getCancelledActions = getCancelledActions; exports.hasRolloutPeriodPassed = void 0; var _moment = _interopRequireDefault(require("moment")); var _constants = require("../../constants"); var _common = require("../../../common"); var _ = require(".."); /* * 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. */ /** * Return current bulk actions */ async function getActionStatuses(esClient, options) { const actions = await _getActions(esClient, options); const cancelledActions = await getCancelledActions(esClient); let acks; try { acks = await esClient.search({ index: _common.AGENT_ACTIONS_RESULTS_INDEX, query: { bool: { // There's some perf/caching advantages to using filter over must // See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#filter-context filter: [{ terms: { action_id: actions.map(a => a.actionId) } }] } }, size: 0, aggs: { ack_counts: { terms: { field: 'action_id', size: actions.length || 10 }, aggs: { max_timestamp: { max: { field: '@timestamp' } } } } } }); } catch (err) { if (err.statusCode === 404) { // .fleet-actions-results does not yet exist _.appContextService.getLogger().debug(err); } else { throw err; } } const results = []; for (const action of actions) { var _acks, _acks$aggregations, _acks$aggregations$ac, _acks$aggregations$ac2, _matchingBucket$doc_c, _matchingBucket$max_t; const matchingBucket = (_acks = acks) === null || _acks === void 0 ? void 0 : (_acks$aggregations = _acks.aggregations) === null || _acks$aggregations === void 0 ? void 0 : (_acks$aggregations$ac = _acks$aggregations.ack_counts) === null || _acks$aggregations$ac === void 0 ? void 0 : (_acks$aggregations$ac2 = _acks$aggregations$ac.buckets) === null || _acks$aggregations$ac2 === void 0 ? void 0 : _acks$aggregations$ac2.find(bucket => bucket.key === action.actionId); const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated; const docCount = (_matchingBucket$doc_c = matchingBucket === null || matchingBucket === void 0 ? void 0 : matchingBucket.doc_count) !== null && _matchingBucket$doc_c !== void 0 ? _matchingBucket$doc_c : 0; const nbAgentsAck = Math.min(docCount, nbAgentsActioned); const completionTime = matchingBucket === null || matchingBucket === void 0 ? void 0 : (_matchingBucket$max_t = matchingBucket.max_timestamp) === null || _matchingBucket$max_t === void 0 ? void 0 : _matchingBucket$max_t.value_as_string; const complete = nbAgentsAck >= nbAgentsActioned; const cancelledAction = cancelledActions.find(a => a.actionId === action.actionId); let errorCount = 0; let latestErrors = []; try { var _ref, _hits$hits, _errorResults$aggrega, _errorResults$aggrega2; // query to find errors in action results, cannot do aggregation on text type const errorResults = await esClient.search({ index: _common.AGENT_ACTIONS_RESULTS_INDEX, track_total_hits: true, rest_total_hits_as_int: true, query: { bool: { must: [{ term: { action_id: action.actionId } }], should: [{ exists: { field: 'error' } }], minimum_should_match: 1 } }, size: 0, aggs: { top_error_hits: { top_hits: { sort: [{ '@timestamp': { order: 'desc' } }], _source: { includes: ['@timestamp', 'agent_id', 'error'] }, size: options.errorSize } } } }); errorCount = (_ref = errorResults.hits.total) !== null && _ref !== void 0 ? _ref : 0; latestErrors = ((_hits$hits = (_errorResults$aggrega = errorResults.aggregations) === null || _errorResults$aggrega === void 0 ? void 0 : (_errorResults$aggrega2 = _errorResults$aggrega.top_error_hits) === null || _errorResults$aggrega2 === void 0 ? void 0 : _errorResults$aggrega2.hits.hits) !== null && _hits$hits !== void 0 ? _hits$hits : []).map(hit => ({ agentId: hit._source.agent_id, error: hit._source.error, timestamp: hit._source['@timestamp'] })); if (latestErrors.length > 0) { const hostNames = await getHostNames(esClient, latestErrors.map(errorItem => errorItem.agentId)); latestErrors.forEach(errorItem => { var _hostNames$errorItem$; errorItem.hostname = (_hostNames$errorItem$ = hostNames[errorItem.agentId]) !== null && _hostNames$errorItem$ !== void 0 ? _hostNames$errorItem$ : errorItem.agentId; }); } } catch (err) { if (err.statusCode === 404) { // .fleet-actions-results does not yet exist _.appContextService.getLogger().debug(err); } else { throw err; } } results.push({ ...action, nbAgentsAck: nbAgentsAck - errorCount, nbAgentsFailed: errorCount, status: cancelledAction ? 'CANCELLED' : errorCount > 0 && complete ? 'FAILED' : complete ? 'COMPLETE' : action.status, nbAgentsActioned, cancellationTime: cancelledAction === null || cancelledAction === void 0 ? void 0 : cancelledAction.timestamp, completionTime, latestErrors }); } const policyChangeActions = await getPolicyChangeActions(esClient); return [...results, ...policyChangeActions].sort((a, b) => b.creationTime > a.creationTime ? 1 : -1); } async function getHostNames(esClient, agentIds) { const agentsRes = await esClient.search({ index: _common.AGENTS_INDEX, query: { bool: { filter: { terms: { 'agent.id': agentIds } } } }, size: agentIds.length, _source: ['local_metadata.host.name'] }); const hostNames = agentsRes.hits.hits.reduce((acc, curr) => { acc[curr._id] = curr._source.local_metadata.host.name; return acc; }, {}); return hostNames; } async function getCancelledActions(esClient) { const res = await esClient.search({ index: _common.AGENT_ACTIONS_INDEX, ignore_unavailable: true, size: _constants.SO_SEARCH_LIMIT, query: { bool: { filter: [{ term: { type: 'CANCEL' } }] } } }); return res.hits.hits.map(hit => { var _hit$_source, _hit$_source$data, _hit$_source2; return { actionId: (_hit$_source = hit._source) === null || _hit$_source === void 0 ? void 0 : (_hit$_source$data = _hit$_source.data) === null || _hit$_source$data === void 0 ? void 0 : _hit$_source$data.target_id, timestamp: (_hit$_source2 = hit._source) === null || _hit$_source2 === void 0 ? void 0 : _hit$_source2['@timestamp'] }; }); } async function _getActions(esClient, options) { var _options$page, _options$perPage; const res = await esClient.search({ index: _common.AGENT_ACTIONS_INDEX, ignore_unavailable: true, from: (_options$page = options.page) !== null && _options$page !== void 0 ? _options$page : 0, size: (_options$perPage = options.perPage) !== null && _options$perPage !== void 0 ? _options$perPage : 20, query: { bool: { must_not: [{ term: { type: 'CANCEL' } }] } }, body: { sort: [{ '@timestamp': 'desc' }] } }); return Object.values(res.hits.hits.reduce((acc, hit) => { var _hit$_source$agents$l, _hit$_source$agents; if (!hit._source || !hit._source.action_id) { return acc; } const source = hit._source; if (!acc[source.action_id]) { var _hit$_source$data2, _source$total, _source$data; const isExpired = source.expiration && source.type !== 'UPGRADE' ? Date.parse(source.expiration) < Date.now() : false; acc[hit._source.action_id] = { actionId: hit._source.action_id, nbAgentsActionCreated: 0, nbAgentsAck: 0, version: (_hit$_source$data2 = hit._source.data) === null || _hit$_source$data2 === void 0 ? void 0 : _hit$_source$data2.version, startTime: source.start_time, type: source.type, nbAgentsActioned: (_source$total = source.total) !== null && _source$total !== void 0 ? _source$total : 0, status: isExpired ? 'EXPIRED' : hasRolloutPeriodPassed(source) ? 'ROLLOUT_PASSED' : 'IN_PROGRESS', expiration: source.expiration, newPolicyId: (_source$data = source.data) === null || _source$data === void 0 ? void 0 : _source$data.policy_id, creationTime: source['@timestamp'], nbAgentsFailed: 0, hasRolloutPeriod: !!source.rollout_duration_seconds }; } acc[hit._source.action_id].nbAgentsActionCreated += (_hit$_source$agents$l = (_hit$_source$agents = hit._source.agents) === null || _hit$_source$agents === void 0 ? void 0 : _hit$_source$agents.length) !== null && _hit$_source$agents$l !== void 0 ? _hit$_source$agents$l : 0; return acc; }, {})); } const hasRolloutPeriodPassed = source => { var _source$start_time; return source.type === 'UPGRADE' && source.rollout_duration_seconds ? Date.now() > (0, _moment.default)((_source$start_time = source.start_time) !== null && _source$start_time !== void 0 ? _source$start_time : Date.now()).add(source.rollout_duration_seconds, 'seconds').valueOf() : false; }; exports.hasRolloutPeriodPassed = hasRolloutPeriodPassed; async function getPolicyChangeActions(esClient) { const latestAgentPoliciesRes = await esClient.search({ index: _common.AGENT_POLICY_INDEX, size: 10, query: { bool: { filter: [{ range: { revision_idx: { gt: 1 } } }] } }, sort: [{ '@timestamp': { order: 'desc' } }], _source: ['revision_idx', '@timestamp', 'policy_id'] }); const latestAgentPolicies = latestAgentPoliciesRes.hits.hits.reduce((acc, curr) => { const hit = curr._source; acc[`${hit.policy_id}:${hit.revision_idx}`] = { policyId: hit.policy_id, revision: hit.revision_idx, timestamp: hit['@timestamp'], agentsAssignedToPolicy: 0, agentsOnAtLeastThisRevision: 0 }; return acc; }, {}); const agentsPerPolicyRevisionRes = await esClient.search({ index: _common.AGENTS_INDEX, size: 0, // ignore unenrolled agents query: { bool: { must_not: [{ exists: { field: 'unenrolled_at' } }] } }, aggs: { policies: { terms: { field: 'policy_id', size: 10 }, aggs: { agents_per_rev: { terms: { field: 'policy_revision_idx', size: 10 } } } } } }); const agentsPerPolicyRevisionMap = agentsPerPolicyRevisionRes.aggregations.policies.buckets.reduce((acc, policyBucket) => { const policyId = policyBucket.key; const policyAgentCount = policyBucket.doc_count; if (!acc[policyId]) acc[policyId] = { total: 0, agentsPerRev: [] }; acc[policyId].total = policyAgentCount; acc[policyId].agentsPerRev = policyBucket.agents_per_rev.buckets.map(agentsPerRev => { return { revision: agentsPerRev.key, agents: agentsPerRev.doc_count }; }); return acc; }, {}); Object.values(latestAgentPolicies).forEach(agentPolicyRev => { const agentsPerPolicyRev = agentsPerPolicyRevisionMap[agentPolicyRev.policyId]; if (agentsPerPolicyRev) { agentPolicyRev.agentsAssignedToPolicy = agentsPerPolicyRev.total; agentsPerPolicyRev.agentsPerRev.forEach(item => { if (agentPolicyRev.revision <= item.revision) { agentPolicyRev.agentsOnAtLeastThisRevision += item.agents; } }); } }); const agentPolicyUpdateActions = Object.entries(latestAgentPolicies).map(([updateKey, updateObj]) => { return { actionId: updateKey, creationTime: updateObj.timestamp, completionTime: updateObj.timestamp, type: 'POLICY_CHANGE', nbAgentsActioned: updateObj.agentsAssignedToPolicy, nbAgentsAck: updateObj.agentsOnAtLeastThisRevision, nbAgentsActionCreated: updateObj.agentsAssignedToPolicy, nbAgentsFailed: 0, status: updateObj.agentsAssignedToPolicy === updateObj.agentsOnAtLeastThisRevision ? 'COMPLETE' : 'IN_PROGRESS', policyId: updateObj.policyId, revision: updateObj.revision }; }); return agentPolicyUpdateActions; }