mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Report automatic upgrade status in UI (#215069)
Closes [4718](https://github.com/elastic/ingest-dev/issues/4718?reload=1?reload=1?reload=1%3Freload%3D1) ## Summary - After much discussion, updated to not show percentages but be very similar to existing upgrade text, but with an icon/tooltip letting the user know it was created from an automatic upgrade. Also applied to completed actions. - Made `policyId` persistent in newly created actions in order to streamline accessing the upgrade modal from agent activity - Added `is_automatic` field to `ActionStatus` type by retrieving from the source doc in `getActions` - Updated audit log to show if the action was created by the user or from the auto-upgrade functionality - Updated badging on table to show retry attempts - Added `manage auto-upgrade` button to agent activity actions created by automatic upgrades - Updated check in `automatic_upgrade_task` to only consider active agents in order to resolve an issue where uninstalled agents could affect the upgrade of new ones. - Reworked rounding functionality when percentages or counts of agents to upgrade were over or under where they should be. Rounding is now done in a way such that no agents get left behind, and we dont try to upgrade more than exist. - Added new test coverage for the rounding functionality, the new active vs inactive agents check, as well as ensuring the manage auto-upgrades button always renders. Simplified UI with tooltip and button to quickly access auto-upgrade settings for the policy the action belongs to:  Updated tooltip to let the user know that rounding is in place:  ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks N/A --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
b920c645c2
commit
a46e8114a2
27 changed files with 558 additions and 169 deletions
|
@ -19679,6 +19679,9 @@
|
|||
"hasRolloutPeriod": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_automatic": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"latestErrors": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -19679,6 +19679,9 @@
|
|||
"hasRolloutPeriod": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_automatic": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"latestErrors": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -22994,6 +22994,8 @@ paths:
|
|||
type: string
|
||||
hasRolloutPeriod:
|
||||
type: boolean
|
||||
is_automatic:
|
||||
type: boolean
|
||||
latestErrors:
|
||||
items:
|
||||
additionalProperties: false
|
||||
|
|
|
@ -25231,6 +25231,8 @@ paths:
|
|||
type: string
|
||||
hasRolloutPeriod:
|
||||
type: boolean
|
||||
is_automatic:
|
||||
type: boolean
|
||||
latestErrors:
|
||||
items:
|
||||
additionalProperties: false
|
||||
|
|
|
@ -18461,8 +18461,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "La configuration des intégrations en exécutant Elastic Agent en mode autonome est une opération avancée. Si possible, nous vous conseillons d'utiliser plutôt {link}.",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "Enregistrer plutôt dans Fleet (recommandé)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "Le monitoring des agents n'est pas activé pour cette politique d'agent. Visitez les paramètres de politique d'agent pour activer le monitoring.",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "Politique modifiée",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "Consultez ici et à tout moment l'historique des activités des agents en cours, terminées et planifiées.",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "Historique des activités des agents",
|
||||
|
@ -18492,7 +18490,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "Démarré le {date}",
|
||||
"xpack.fleet.agentActivityFlyout.title": "Activité des agents",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "Aujourd'hui",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "{guideLink} concernant les mises à jour de l'agent.",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "Afficher les agents",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "La fonctionnalité d'affichage des agents est disponible uniquement pour une action affectant moins de {agentCount} agents",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "Afficher les agents actuellement affectés à cette politique",
|
||||
|
|
|
@ -18437,8 +18437,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "スタンドアロンモードでElasticエージェントを実行して統合を設定する方法は、上級者向けです。可能なかぎり、{link}を使用することをお勧めします。",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "Fleetで登録(推奨)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "このエージェントポリシーのアラート監視が無効です。エージェントポリシー設定にアクセスし、監視を有効化してください。",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "ポリシーが変更されました",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "実行中、完了、スケジュール済みのエージェントアクションアクティビティ履歴をいつでもここで確認できます。",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "エージェントアクティビティ履歴",
|
||||
|
@ -18468,7 +18466,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "{date}に開始しました。",
|
||||
"xpack.fleet.agentActivityFlyout.title": "エージェントアクティビティ",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "今日",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "エージェントアップグレードについては、{guideLink}。",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "エージェントを表示",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "エージェントの表示機能は、{agentCount}個未満のエージェントに影響するアクションでのみ利用可能です",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "現在このポリシーに割り当てられているエージェントを表示",
|
||||
|
|
|
@ -18478,8 +18478,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "通过在独立模式下运行 Elastic 代理来设置集成为高级选项。如果可能,我们建议改用 {link}。",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "改为在 Fleet 中注册(建议)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "未对此代理策略启用代理监测。访问代理策略设置以启用监测。",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "策略已更改",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "随时在此处查看正在进行、已完成和已计划的代理操作活动历史记录。",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "代理活动历史记录",
|
||||
|
@ -18509,7 +18507,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "已于 {date}启动。",
|
||||
"xpack.fleet.agentActivityFlyout.title": "代理活动",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "今日",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "有关代理升级的{guideLink}。",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "查看代理",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "查看代理功能仅适用于影响的代理数不超过 {agentCount} 个的操作",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "查看当前分配给此策略的代理",
|
||||
|
|
|
@ -73,6 +73,7 @@ export interface NewAgentAction {
|
|||
source_uri?: string;
|
||||
total?: number;
|
||||
is_automatic?: boolean;
|
||||
policyId?: string;
|
||||
}
|
||||
|
||||
export interface AgentAction extends NewAgentAction {
|
||||
|
@ -187,6 +188,7 @@ export interface ActionStatus {
|
|||
latestErrors?: ActionErrorResult[];
|
||||
revision?: number;
|
||||
policyId?: string;
|
||||
is_automatic?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentDiagnostics {
|
||||
|
@ -462,6 +464,9 @@ export interface FleetServerAgentAction {
|
|||
*/
|
||||
is_automatic?: boolean;
|
||||
|
||||
// the id of the policy associated with the action
|
||||
policyId?: string;
|
||||
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,10 @@ import {
|
|||
EuiPanel,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
EuiIconTip,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
|
@ -26,12 +29,23 @@ import { ViewErrors } from '../view_errors';
|
|||
import { formattedTime, getAction, inProgressDescription, inProgressTitle } from './helpers';
|
||||
import { ViewAgentsButton } from './view_agents_button';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 50%;
|
||||
border-left: ${(props) => props.theme.euiTheme.border.thin};
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
export const ActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
const theme = useEuiTheme();
|
||||
|
||||
const isAutomaticUpgrade = action.is_automatic;
|
||||
const completeTitle =
|
||||
action.type === 'POLICY_CHANGE' && action.nbAgentsActioned === 0 ? (
|
||||
<EuiText>
|
||||
|
@ -41,26 +55,43 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
/>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.completedTitle"
|
||||
defaultMessage="{nbAgents} {agents} {completedText}{offlineText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
completedText: getAction(action.type, action.actionId).completedText,
|
||||
offlineText:
|
||||
action.status === 'ROLLOUT_PASSED' && action.nbAgentsActioned - action.nbAgentsAck > 0
|
||||
? `, ${
|
||||
action.nbAgentsActioned - action.nbAgentsAck
|
||||
} agent(s) offline during the rollout period`
|
||||
: '',
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.completedTitle"
|
||||
defaultMessage="{nbAgents} {agents} {completedText}{versionText}{offlineText}{automaticIcon}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
completedText: getAction(action.type, action.actionId).completedText,
|
||||
offlineText:
|
||||
action.status === 'ROLLOUT_PASSED' &&
|
||||
action.nbAgentsActioned - action.nbAgentsAck > 0
|
||||
? `, ${
|
||||
action.nbAgentsActioned - action.nbAgentsAck
|
||||
} agent(s) offline during the rollout period`
|
||||
: '',
|
||||
versionText: action.version ? ` to version ${action.version}` : '',
|
||||
automaticIcon: action.is_automatic ? (
|
||||
<EuiIconTip
|
||||
anchorProps={{
|
||||
style: { display: 'flex', alignItems: 'center' },
|
||||
}}
|
||||
type="timeRefresh"
|
||||
content="Triggered by an automatic upgrade"
|
||||
/>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
// TODO: investigate whether default completion is due to a bug
|
||||
|
@ -100,7 +131,7 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
} = {
|
||||
IN_PROGRESS: {
|
||||
icon: <EuiLoadingSpinner size="m" />,
|
||||
title: <EuiText>{inProgressTitle(action)}</EuiText>,
|
||||
title: <EuiText>{inProgressTitle(action, action.is_automatic)}</EuiText>,
|
||||
titleColor: theme.euiTheme.colors.textPrimary,
|
||||
description: <EuiText color="subdued">{inProgressDescription(action.creationTime)}</EuiText>,
|
||||
},
|
||||
|
@ -247,7 +278,31 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
|
||||
{isAutomaticUpgrade && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="manageAutoUpgradesButton"
|
||||
onClick={() => onClickManageAutoUpgradeAgents(action)}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.manageAutoUpgradeAgents"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiText, EuiPanel } from '@elastic/eui';
|
||||
import { EuiText, EuiPanel, EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
|
@ -19,12 +19,22 @@ export const ActivitySection: React.FunctionComponent<{
|
|||
actions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ title, actions, abortUpgrade, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ title, actions, abortUpgrade, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiPanel color="subdued" hasBorder={true} borderRadius="none">
|
||||
<EuiText>
|
||||
<b>{title}</b>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
{actions.some((action) => action.status === 'IN_PROGRESS') && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color="success" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<b>{title}</b>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
{actions.map((currentAction) =>
|
||||
|
@ -34,12 +44,14 @@ export const ActivitySection: React.FunctionComponent<{
|
|||
abortUpgrade={abortUpgrade}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : (
|
||||
<ActivityItem
|
||||
action={currentAction}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
@ -47,6 +47,8 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
currentActions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => Promise<void>;
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
|
||||
areActionsFullyLoaded: boolean;
|
||||
onClickShowMore: () => void;
|
||||
dateFilter: moment.Moment | null;
|
||||
|
@ -56,6 +58,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
currentActions,
|
||||
abortUpgrade,
|
||||
onClickViewAgents,
|
||||
onClickManageAutoUpgradeAgents,
|
||||
areActionsFullyLoaded,
|
||||
onClickShowMore,
|
||||
dateFilter,
|
||||
|
@ -140,7 +143,9 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
}
|
||||
|
||||
// Loaded actions
|
||||
|
||||
const inProgressActions = currentActions.filter((a) => a.status === 'IN_PROGRESS');
|
||||
|
||||
const completedActions = currentActions.filter((a) => a.status !== 'IN_PROGRESS');
|
||||
const todayActions = getTodayActions(completedActions);
|
||||
const otherDays = getOtherDaysActions(completedActions);
|
||||
|
@ -162,6 +167,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={inProgressActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : null}
|
||||
{todayActions.length > 0 ? (
|
||||
|
@ -175,6 +181,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={todayActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : null}
|
||||
{Object.keys(otherDays).map((day) => (
|
||||
|
@ -184,6 +191,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={otherDays[day]}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
import { EuiIconTip, EuiFlexGroup } from '@elastic/eui';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
|
@ -71,25 +72,36 @@ export const getAction = (type?: string, actionId?: string) => {
|
|||
return actionNames[type ?? 'ACTION'] ?? actionNames.ACTION;
|
||||
};
|
||||
|
||||
export const inProgressTitle = (action: ActionStatus) => (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.inProgressTitle"
|
||||
defaultMessage="{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck >= action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck === 0
|
||||
? action.nbAgentsActioned
|
||||
: action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
inProgressText: getAction(action.type, action.actionId).inProgressText,
|
||||
reassignText:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '',
|
||||
upgradeText: action.type === 'UPGRADE' ? `to version ${action.version}` : '',
|
||||
failuresText: action.nbAgentsFailed > 0 ? `, has ${action.nbAgentsFailed} failure(s)` : '',
|
||||
}}
|
||||
/>
|
||||
export const inProgressTitle = (action: ActionStatus, isAutomatic: boolean | undefined) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.inProgressTitle"
|
||||
defaultMessage="{inProgressText} {nbAgents} {agents}{reassignText}{upgradeText}{failuresText}{automaticIcon}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck >= action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck === 0
|
||||
? action.nbAgentsActioned
|
||||
: action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
inProgressText: getAction(action.type, action.actionId).inProgressText,
|
||||
reassignText:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '',
|
||||
upgradeText: action.type === 'UPGRADE' ? ` to version ${action.version}` : '',
|
||||
failuresText: action.nbAgentsFailed > 0 ? `, has ${action.nbAgentsFailed} failure(s)` : '',
|
||||
automaticIcon: isAutomatic ? (
|
||||
<EuiIconTip
|
||||
anchorProps={{
|
||||
style: { display: 'flex', alignItems: 'center' },
|
||||
}}
|
||||
type="timeRefresh"
|
||||
content="Triggered by an automatic upgrade"
|
||||
/>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
export const inProgressDescription = (time?: string) => (
|
||||
|
|
|
@ -6,17 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render, fireEvent } from '@testing-library/react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
import type { TestRenderer } from '../../../../../../../mock';
|
||||
import { createFleetTestRendererMock } from '../../../../../../../mock';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { useActionStatus } from '../../hooks';
|
||||
import { useGetAgentPolicies, useStartServices, useAuthz } from '../../../../../hooks';
|
||||
import { useGetAgentPolicies, useAuthz } from '../../../../../hooks';
|
||||
|
||||
import { AgentActivityFlyout } from '.';
|
||||
|
||||
jest.mock('../../hooks');
|
||||
jest.mock('../../../../../hooks');
|
||||
jest.mock('../../../../../../../hooks/use_request/agent_policy');
|
||||
jest.mock('../../../../../../../hooks/use_authz');
|
||||
|
||||
jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
|
||||
RedirectAppLinks: ({ children }: { children: React.ReactNode }) => children,
|
||||
|
@ -24,7 +28,7 @@ jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
|
|||
|
||||
const mockUseActionStatus = useActionStatus as jest.Mock;
|
||||
const mockUseGetAgentPolicies = useGetAgentPolicies as jest.Mock;
|
||||
const mockUseStartServices = useStartServices as jest.Mock;
|
||||
|
||||
const mockedUseAuthz = useAuthz as jest.Mock;
|
||||
|
||||
jest.mock('@kbn/logs-shared-plugin/common', () => {
|
||||
|
@ -43,6 +47,7 @@ describe('AgentActivityFlyout', () => {
|
|||
const mockAbortUpgrade = jest.fn();
|
||||
const mockSetSearch = jest.fn();
|
||||
const mockSetSelectedStatus = jest.fn();
|
||||
const mockOpenManageAutoUpgradeModal = jest.fn();
|
||||
|
||||
const component = (refreshAgentActivity: boolean = false) => (
|
||||
<IntlProvider timeZone="UTC" locale="en">
|
||||
|
@ -52,11 +57,13 @@ describe('AgentActivityFlyout', () => {
|
|||
refreshAgentActivity={refreshAgentActivity}
|
||||
setSearch={mockSetSearch}
|
||||
setSelectedStatus={mockSetSelectedStatus}
|
||||
openManageAutoUpgradeModal={mockOpenManageAutoUpgradeModal}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
let testRenderer: TestRenderer;
|
||||
beforeEach(() => {
|
||||
testRenderer = createFleetTestRendererMock();
|
||||
mockOnClose.mockReset();
|
||||
mockOnAbortSuccess.mockReset();
|
||||
mockAbortUpgrade.mockReset();
|
||||
|
@ -66,30 +73,18 @@ describe('AgentActivityFlyout', () => {
|
|||
mockUseGetAgentPolicies.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{ id: 'policy1', name: 'Policy 1' },
|
||||
{ id: 'policy2', name: 'Policy 2' },
|
||||
{ id: 'Policy1', name: 'Policy 1' },
|
||||
{ id: 'Policy2', name: 'Policy 2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
mockUseStartServices.mockReturnValue({
|
||||
docLinks: { links: { fleet: { upgradeElasticAgent: 'https://elastic.co' } } },
|
||||
application: { navigateToUrl: jest.fn() },
|
||||
http: { basePath: { prepend: jest.fn() } },
|
||||
share: {
|
||||
url: {
|
||||
locators: {
|
||||
get: () => ({
|
||||
useUrl: () => 'https://locator.url',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockedUseAuthz.mockReturnValue({
|
||||
fleet: {
|
||||
readAgents: true,
|
||||
allAgents: true,
|
||||
},
|
||||
integrations: {},
|
||||
} as any);
|
||||
});
|
||||
|
||||
|
@ -118,24 +113,22 @@ describe('AgentActivityFlyout', () => {
|
|||
hasRolloutPeriod: true,
|
||||
},
|
||||
];
|
||||
mockUseActionStatus
|
||||
.mockReturnValueOnce({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: true,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: true,
|
||||
});
|
||||
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('loading')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('upgradeInProgressTitle')).not.toBeInTheDocument();
|
||||
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
result.rerender(component());
|
||||
expect(result.queryByTestId('loading')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('upgradeInProgressTitle')).toBeInTheDocument();
|
||||
|
@ -147,7 +140,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.queryByText('No activity to display')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -174,7 +167,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -186,7 +179,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(result.getByText('Cancel'));
|
||||
|
@ -217,7 +210,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -229,7 +222,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
expect(result.queryByText('Cancel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -255,7 +248,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -266,7 +259,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Scheduled for Sep 16, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Scheduled for Sep 16, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(result.getByText('Cancel'));
|
||||
|
@ -295,7 +288,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'2 agents upgraded'
|
||||
|
@ -326,7 +319,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'1 of 2 agents upgraded, 1 agent(s) offline during the rollout period'
|
||||
|
@ -357,7 +350,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'1 of 2 agents upgraded, 1 agent(s) offline during the rollout period'
|
||||
|
@ -393,7 +386,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Agent unenrollment expired'
|
||||
|
@ -426,7 +419,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Agent upgrade cancelled'
|
||||
|
@ -448,7 +441,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 1,
|
||||
status: 'FAILED',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
newPolicyId: 'policy1',
|
||||
newPolicyId: 'Policy1',
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 1,
|
||||
completionTime: '2022-09-15T11:00:00.000Z',
|
||||
|
@ -459,7 +452,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'0 of 1 agent assigned to a new policy'
|
||||
|
@ -496,7 +489,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'0 of 3 agents actioned'
|
||||
|
@ -518,7 +511,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 0,
|
||||
status: 'COMPLETE',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'policy1',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
|
@ -530,7 +523,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Policy changed'
|
||||
|
@ -552,7 +545,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 3,
|
||||
status: 'COMPLETE',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'policy1',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
|
@ -564,7 +557,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'3 agents applied policy change'
|
||||
|
@ -576,6 +569,42 @@ describe('AgentActivityFlyout', () => {
|
|||
).toContain('Policy1 changed to revision 2 at Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
it('should render agent activity for an automatic upgrade', () => {
|
||||
const mockActionStatuses = [
|
||||
{
|
||||
actionId: 'action8',
|
||||
nbAgentsActionCreated: 3,
|
||||
nbAgentsAck: 0,
|
||||
type: 'UPGRADE',
|
||||
nbAgentsActioned: 3,
|
||||
is_automatic: true,
|
||||
status: 'IN_PROGRESS',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
completionTime: '2022-09-15T11:00:00.000Z',
|
||||
version: '8.17.3',
|
||||
},
|
||||
];
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(
|
||||
result.container.querySelector('[data-test-subj="upgradeInProgressTitle"]')!.textContent
|
||||
).toContain('Upgrading 3 agents to version 8.17.3');
|
||||
expect(
|
||||
result.container
|
||||
.querySelector('[data-test-subj="manageAutoUpgradesButton"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Manage auto-upgrade agents'.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
it('should keep flyout state on new data', () => {
|
||||
const failedAction = {
|
||||
actionId: 'action1',
|
||||
|
@ -626,7 +655,7 @@ describe('AgentActivityFlyout', () => {
|
|||
};
|
||||
}
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
expect(result.container.querySelector('[data-test-subj="upgradeInProgressTitle"]')).toBe(null);
|
||||
|
|
|
@ -46,7 +46,15 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
refreshAgentActivity: boolean;
|
||||
setSearch: (search: string) => void;
|
||||
setSelectedStatus: (status: string[]) => void;
|
||||
}> = ({ onClose, onAbortSuccess, refreshAgentActivity, setSearch, setSelectedStatus }) => {
|
||||
openManageAutoUpgradeModal: (policyId: string) => void;
|
||||
}> = ({
|
||||
onClose,
|
||||
onAbortSuccess,
|
||||
refreshAgentActivity,
|
||||
setSearch,
|
||||
setSelectedStatus,
|
||||
openManageAutoUpgradeModal,
|
||||
}) => {
|
||||
const { notifications } = useStartServices();
|
||||
const { data: agentPoliciesData } = useGetAgentPolicies({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
|
@ -75,7 +83,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
currentActions.map((a) => ({
|
||||
...a,
|
||||
newPolicyId: getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
policyId: getAgentPolicyName(a.policyId ?? ''),
|
||||
policyId: a.policyId ? a.policyId : getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
})),
|
||||
[currentActions, getAgentPolicyName]
|
||||
);
|
||||
|
@ -100,6 +108,11 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
});
|
||||
}
|
||||
};
|
||||
const onClickManageAutoUpgradeAgents = async (action: ActionStatus) => {
|
||||
// use the policy id from the action to manage
|
||||
onClose();
|
||||
openManageAutoUpgradeModal(action.policyId!);
|
||||
};
|
||||
|
||||
const onClickShowMore = () => {
|
||||
setNActions(nActions + 10);
|
||||
|
@ -153,6 +166,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
currentActions={currentActionsEnriched}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
areActionsFullyLoaded={areActionsFullyLoaded}
|
||||
onClickShowMore={onClickShowMore}
|
||||
dateFilter={dateFilter}
|
||||
|
|
|
@ -16,24 +16,37 @@ import {
|
|||
EuiButton,
|
||||
EuiLink,
|
||||
useEuiTheme,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
import { useStartServices } from '../../../../../hooks';
|
||||
|
||||
import { formattedTime, inProgressDescription, inProgressTitle } from './helpers';
|
||||
|
||||
import { ViewAgentsButton } from './view_agents_button';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 50%;
|
||||
border-left: ${(props) => props.theme.euiTheme.border.thin};
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, abortUpgrade, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, abortUpgrade, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const theme = useEuiTheme();
|
||||
|
||||
const isAutomaticUpgrade = action.is_automatic;
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const onClickAbortUpgrade = useCallback(async () => {
|
||||
try {
|
||||
|
@ -61,6 +74,23 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
return (
|
||||
<EuiPanel hasBorder={true} borderRadius="none">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" data-test-subj="upgradeInProgressDescription">
|
||||
<p>
|
||||
{isScheduled && action.startTime ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduledDescription"
|
||||
defaultMessage="Scheduled for "
|
||||
/>
|
||||
<strong>{formattedTime(action.startTime)}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>{inProgressDescription(action.creationTime)} </>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -81,7 +111,7 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
inProgressTitle(action)
|
||||
inProgressTitle(action, isAutomaticUpgrade)
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -89,40 +119,45 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" data-test-subj="upgradeInProgressDescription">
|
||||
<p>
|
||||
{isScheduled && action.startTime ? (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
{isAutomaticUpgrade && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="manageAutoUpgradesButton"
|
||||
onClick={() => onClickManageAutoUpgradeAgents(action)}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduledDescription"
|
||||
defaultMessage="Scheduled for "
|
||||
id="xpack.fleet.agentActivityFlyout.manageAutoUpgradeAgents"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
<strong>{formattedTime(action.startTime)}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>{inProgressDescription(action.creationTime)} </>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.upgradeDescription"
|
||||
defaultMessage="{guideLink} about agent upgrades."
|
||||
values={{
|
||||
guideLink: (
|
||||
<EuiLink href={docLinks.links.fleet.upgradeElasticAgent} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.guideLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty>
|
||||
<EuiLink href={docLinks.links.fleet.upgradeElasticAgent} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.guideLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{showCancelButton ? (
|
||||
<EuiButton
|
||||
|
|
|
@ -29,7 +29,6 @@ export const ViewAgentsButton: React.FunctionComponent<{
|
|||
<EuiButtonEmpty
|
||||
size="m"
|
||||
onClick={() => onClickViewAgents(action)}
|
||||
flush="left"
|
||||
data-test-subj="agentActivityFlyout.viewAgentsButton"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
isAgentUpgradeAvailable,
|
||||
} from '../../../../../../../common/services';
|
||||
|
||||
import { AUTO_UPGRADE_DEFAULT_RETRIES } from '../../../../../../../common/constants';
|
||||
|
||||
import { useConfig } from '../../../../hooks';
|
||||
|
||||
/**
|
||||
* Returns a user-friendly string for the estimated remaining time until the upgrade is scheduled.
|
||||
*/
|
||||
|
@ -282,7 +286,7 @@ export const AgentUpgradeStatus: React.FC<{
|
|||
const status = useMemo(() => getStatusComponents(agent.upgrade_details), [agent.upgrade_details]);
|
||||
const minVersion = '8.12';
|
||||
const notUpgradeableMessage = getNotUpgradeableMessage(agent, latestAgentVersion);
|
||||
|
||||
const retryDelays = useConfig().autoUpgrades?.retryDelays ?? AUTO_UPGRADE_DEFAULT_RETRIES;
|
||||
if (agent.upgrade_details && status) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
|
@ -300,6 +304,28 @@ export const AgentUpgradeStatus: React.FC<{
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
if (agent.upgrade_attempts && agent.upgrade_attempts.length > 1 && agent.status === 'updating') {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUpgradeStatusTooltip.retryingUpgrade"
|
||||
defaultMessage="Retrying Upgrade ({retryCount}/{maxRetries} attempts)"
|
||||
values={{
|
||||
retryCount: agent.upgrade_attempts.length,
|
||||
maxRetries: retryDelays.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
color="subdued"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAgentUpgradable && isAgentUpgradeAvailable(agent, latestAgentVersion)) {
|
||||
return (
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy';
|
||||
|
||||
import { AgentRequestDiagnosticsModal } from '../components/agent_request_diagnostics_modal';
|
||||
import { ManageAutoUpgradeAgentsModal } from '../components/manage_auto_upgrade_agents_modal';
|
||||
|
||||
import type { SelectionMode } from './components/types';
|
||||
|
||||
|
@ -62,6 +63,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
isOpen: false,
|
||||
});
|
||||
const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false);
|
||||
const [isManageAutoUpgradeModalOpen, setManageAutoUpgradeModalOpen] = useState(false);
|
||||
|
||||
const [selectedPolicyId, setSelectedPolicyId] = useState<string | undefined>();
|
||||
const flyoutContext = useFlyoutContext();
|
||||
|
||||
// Agent actions states
|
||||
|
@ -285,12 +289,26 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
<AgentActivityFlyout
|
||||
onAbortSuccess={fetchData}
|
||||
onClose={() => setAgentActivityFlyoutOpen(false)}
|
||||
openManageAutoUpgradeModal={(policyId: string) => {
|
||||
setSelectedPolicyId(policyId);
|
||||
|
||||
setManageAutoUpgradeModalOpen(true);
|
||||
}}
|
||||
refreshAgentActivity={isLoading}
|
||||
setSearch={setSearch}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
{isManageAutoUpgradeModalOpen ? (
|
||||
<EuiPortal>
|
||||
<ManageAutoUpgradeAgentsModal
|
||||
key={selectedPolicyId}
|
||||
onClose={() => setManageAutoUpgradeModalOpen(false)}
|
||||
agentPolicy={allAgentPolicies.find((p) => p.id === selectedPolicyId)!}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
{enrollmentFlyout.isOpen ? (
|
||||
<EuiPortal>
|
||||
<AgentEnrollmentFlyout
|
||||
|
|
|
@ -34,7 +34,7 @@ import { StatusColumn } from './status_column';
|
|||
export interface ManageAutoUpgradeAgentsModalProps {
|
||||
onClose: (refreshPolicy: boolean) => void;
|
||||
agentPolicy: AgentPolicy;
|
||||
agentCount: number;
|
||||
agentCount?: number;
|
||||
}
|
||||
|
||||
export const ManageAutoUpgradeAgentsModal: React.FunctionComponent<
|
||||
|
@ -240,11 +240,12 @@ const TargetVersionsRow: React.FunctionComponent<{
|
|||
/>
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
title={'Rounding Applied'}
|
||||
content={
|
||||
<FormattedMessage
|
||||
data-test-subj="percentageTooltip"
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.percentageTooltip"
|
||||
defaultMessage="Set 100 to upgrade all agents in the policy."
|
||||
defaultMessage="The actual percentage of agents upgraded may vary slightly due to rounding. For example, selecting 30% of 25 agents may result in 8 agents being upgraded (32%)."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useGetAutoUpgradeAgentsStatusQuery, useLink } from '../../../../../../hooks';
|
||||
|
||||
export const StatusColumn: React.FunctionComponent<{
|
||||
agentPolicyId: string;
|
||||
version: string;
|
||||
|
@ -26,7 +25,6 @@ export const StatusColumn: React.FunctionComponent<{
|
|||
}> = ({ agentPolicyId, version, percentage }) => {
|
||||
const { getHref } = useLink();
|
||||
const { data: autoUpgradeAgentsStatus } = useGetAutoUpgradeAgentsStatusQuery(agentPolicyId);
|
||||
|
||||
const getAgentsHref = useCallback(
|
||||
(failed?: boolean): string => {
|
||||
const kuery = failed
|
||||
|
@ -120,7 +118,7 @@ export const StatusColumn: React.FunctionComponent<{
|
|||
agentVersionCounts.agents > 0 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.currentStatusTooltip"
|
||||
defaultMessage="{agents} agents on target version"
|
||||
defaultMessage="{agents, plural, one {# agent} other {# agents}} on target version"
|
||||
values={{
|
||||
agents: agentVersionCounts.agents,
|
||||
}}
|
||||
|
|
|
@ -165,7 +165,6 @@ async function getActionResults(
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
...action,
|
||||
nbAgentsAck: nbAgentsAck - errorCount,
|
||||
|
@ -274,6 +273,8 @@ async function getActions(
|
|||
creationTime: source['@timestamp']!,
|
||||
nbAgentsFailed: 0,
|
||||
hasRolloutPeriod: !!source.rollout_duration_seconds,
|
||||
is_automatic: source.is_automatic,
|
||||
policyId: source.policyId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export async function createAgentAction(
|
|||
total: newAgentAction.total,
|
||||
traceparent: apm.currentTraceparent,
|
||||
is_automatic: newAgentAction.is_automatic,
|
||||
policyId: newAgentAction.policyId,
|
||||
};
|
||||
|
||||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
|
@ -87,7 +88,9 @@ export async function createAgentAction(
|
|||
});
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User created Fleet action [id=${actionId}]`,
|
||||
message: `${
|
||||
newAgentAction.is_automatic ? 'Automatic Upgrade' : 'User'
|
||||
} created Fleet action [id=${actionId}]`,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -194,6 +194,7 @@ export async function upgradeBatch(
|
|||
...rollingUpgradeOptions,
|
||||
namespaces,
|
||||
is_automatic: options.isAutomatic,
|
||||
policyId: agentsToUpdate[0]?.policy_id,
|
||||
});
|
||||
|
||||
await createErrorActionResults(
|
||||
|
|
|
@ -216,7 +216,7 @@ describe('AutomaticAgentUpgradeTask', () => {
|
|||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 2),
|
||||
agents: agents.slice(0, 2), // As theres already one upgrading, and 30% of 11 is 3, we only want two items to be sent for upgrade
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
|
@ -260,6 +260,89 @@ describe('AutomaticAgentUpgradeTask', () => {
|
|||
expect(mockedSendAutomaticUpgradeAgentsActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not take inactive agents into account when checking for upgrade eligibility', async () => {
|
||||
const activeAgents = generateAgents(10, 'agent-policy-1', '8.15.0');
|
||||
const uninstalledAgents = generateAgents(5, 'agent-policy-1', '8.17.3', 'uninstalled');
|
||||
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: activeAgents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery(activeAgents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: activeAgents.slice(0, 3),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{ agents: uninstalledAgents, version: '8.18.0' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly round agent counts and not over or undershoot the target', async () => {
|
||||
// need to check that things are being rounded correctly. Each sub-array would be an automatic upgrade on a policy with a breakout of how many of each version should be upgraded
|
||||
// the target percentage is what we want to hit, but the count is what we have with the first step of normal rounding
|
||||
// this tests that the adjustment to ensure we dont go over or under the total amount of agents is working as intended
|
||||
const MOCK_VERSIONS_AND_COUNTS = [
|
||||
[
|
||||
{ version: '8.17.3', count: 1, targetPercentage: 33, alreadyUpgrading: 0 }, // 3 way split of 4 agents
|
||||
{ version: '8.17.2', count: 1, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 1, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[
|
||||
{ version: '8.17.3', count: 33, targetPercentage: 33, alreadyUpgrading: 0 }, // 3 way split of 99 agents
|
||||
{ version: '8.17.2', count: 33, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 34, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[{ version: '8.17.3', count: 60, targetPercentage: 60, alreadyUpgrading: 0 }], // 60% with 99 agents
|
||||
[
|
||||
{ version: '8.17.3', count: 13, targetPercentage: 50, alreadyUpgrading: 0 },
|
||||
{ version: '8.17.2', count: 13, targetPercentage: 50, alreadyUpgrading: 0 }, // 50% each with 25 agents
|
||||
],
|
||||
];
|
||||
const TOTAL_AGENTS_MOCKS = [4, 99, 99, 25]; // how many total agents each array should be using to recalculate the values
|
||||
|
||||
const MOCK_VERSIONS_AND_COUNTS_EXPECTED = [
|
||||
[
|
||||
{ version: '8.17.3', count: 2, targetPercentage: 33, alreadyUpgrading: 0 }, // since we were missing one
|
||||
{ version: '8.17.2', count: 1, targetPercentage: 33, alreadyUpgrading: 0 }, // we should add one to the lowest
|
||||
|
||||
{ version: '8.17.1', count: 1, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[
|
||||
{ version: '8.17.3', count: 32, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
{ version: '8.17.2', count: 33, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 34, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[{ version: '8.17.3', count: 59, targetPercentage: 60, alreadyUpgrading: 0 }], // since the 60 above was over, this should come out to 59
|
||||
[
|
||||
{ version: '8.17.3', count: 12, targetPercentage: 50, alreadyUpgrading: 0 }, // as theres too many above, it should get reduced by 1
|
||||
{ version: '8.17.2', count: 13, targetPercentage: 50, alreadyUpgrading: 0 },
|
||||
],
|
||||
];
|
||||
// now assert on each item using the above mocks
|
||||
for (let i = 0; i < MOCK_VERSIONS_AND_COUNTS.length; i++) {
|
||||
const result = await mockTask.adjustAgentCounts(
|
||||
MOCK_VERSIONS_AND_COUNTS[i],
|
||||
TOTAL_AGENTS_MOCKS[i]
|
||||
);
|
||||
|
||||
expect(result).toEqual(MOCK_VERSIONS_AND_COUNTS_EXPECTED[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should set a rollout duration for upgrade batches bigger than 10 agents', async () => {
|
||||
const agents = generateAgents(100);
|
||||
mockedGetAgentsByKuery
|
||||
|
|
|
@ -61,6 +61,12 @@ interface AutomaticAgentUpgradeTaskSetupContract {
|
|||
interface AutomaticAgentUpgradeTaskStartContract {
|
||||
taskManager: TaskManagerStartContract;
|
||||
}
|
||||
interface UpgradeTargetForVersion {
|
||||
version: string;
|
||||
count: number;
|
||||
targetPercentage: number;
|
||||
alreadyUpgrading: number;
|
||||
}
|
||||
|
||||
export class AutomaticAgentUpgradeTask {
|
||||
private logger: Logger;
|
||||
|
@ -220,6 +226,13 @@ export class AutomaticAgentUpgradeTask {
|
|||
);
|
||||
return;
|
||||
}
|
||||
// Before processing each required version, we need to get the count of agents for each version so we know if we should round some up or down to make sure we arent overshooting the total number of agents.
|
||||
const versionAndCounts = await this.getVersionAndCounts(
|
||||
agentPolicy,
|
||||
totalActiveAgents,
|
||||
esClient,
|
||||
soClient
|
||||
);
|
||||
|
||||
for (const requiredVersion of agentPolicy.required_versions ?? []) {
|
||||
await this.processRequiredVersion(
|
||||
|
@ -227,10 +240,90 @@ export class AutomaticAgentUpgradeTask {
|
|||
soClient,
|
||||
agentPolicy,
|
||||
requiredVersion,
|
||||
totalActiveAgents
|
||||
versionAndCounts
|
||||
);
|
||||
}
|
||||
}
|
||||
public async getVersionAndCounts(
|
||||
agentPolicy: AgentPolicy,
|
||||
totalActiveAgents: number,
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract
|
||||
) {
|
||||
let versionAndCounts: UpgradeTargetForVersion[] = [];
|
||||
for (const requiredVersion of agentPolicy.required_versions ?? []) {
|
||||
let numberOfAgentsForUpgrade = Math.round(
|
||||
(totalActiveAgents * requiredVersion.percentage) / 100
|
||||
);
|
||||
|
||||
// Subtract the total number of agents already or on or updating to target version.
|
||||
const updatingToKuery = `(upgrade_details.target_version:${requiredVersion.version} AND NOT upgrade_details.state:UPG_FAILED)`;
|
||||
const totalOnOrUpdatingToTargetVersionAgents = await this.getAgentCount(
|
||||
esClient,
|
||||
soClient,
|
||||
`((policy_id:${agentPolicy.id} AND agent.version:${
|
||||
requiredVersion.version
|
||||
}) OR ${updatingToKuery}) AND ${AgentStatusKueryHelper.buildKueryForActiveAgents()}`
|
||||
);
|
||||
|
||||
numberOfAgentsForUpgrade -= totalOnOrUpdatingToTargetVersionAgents;
|
||||
versionAndCounts.push({
|
||||
version: requiredVersion.version,
|
||||
count: numberOfAgentsForUpgrade,
|
||||
targetPercentage: requiredVersion.percentage,
|
||||
alreadyUpgrading: totalOnOrUpdatingToTargetVersionAgents,
|
||||
});
|
||||
}
|
||||
// Then we need to make adjustments based on the total to make sure we arent over or undershooting the total number of agents
|
||||
versionAndCounts = await this.adjustAgentCounts(versionAndCounts, totalActiveAgents);
|
||||
return versionAndCounts;
|
||||
}
|
||||
public async adjustAgentCounts(
|
||||
versionAndCounts: UpgradeTargetForVersion[],
|
||||
totalActiveAgents: number
|
||||
) {
|
||||
// Calculate what we actually have vs what we need to have.
|
||||
// First we need to get the total actual percentage if we actually added the new agents and considering the existing ones
|
||||
const totalActualPercentage =
|
||||
((versionAndCounts.reduce((acc, item) => acc + item.count, 0) +
|
||||
versionAndCounts.reduce((acc, item) => acc + item.alreadyUpgrading, 0)) /
|
||||
totalActiveAgents) *
|
||||
100;
|
||||
const totalNeededPercentage = versionAndCounts.reduce(
|
||||
(acc, item) => acc + item.targetPercentage,
|
||||
0
|
||||
);
|
||||
|
||||
// Now we have the total percentage after we add everything up, vs the total target percentage we have. Get the difference, then multiply that by the total active agents to get the delta we need to add or remove from the total count.
|
||||
const totalDeltaPercentage = totalActualPercentage - totalNeededPercentage;
|
||||
|
||||
// If we are over, we need to remove some from the count, and if we are under, we need to add some to the count. If we're spot on, all good.
|
||||
if (totalDeltaPercentage !== 0) {
|
||||
// get the actual count of agents we are off by using the percentage * the total active agents
|
||||
let deltaCount = Math.round((totalDeltaPercentage / 100) * totalActiveAgents);
|
||||
|
||||
// Now we need to add or remove from the versionAndCounts array
|
||||
let index = 0;
|
||||
// So long as we have more to add or remove, do so
|
||||
while (deltaCount !== 0 && index < versionAndCounts.length) {
|
||||
const item = versionAndCounts[index];
|
||||
if (deltaCount > 0) {
|
||||
// Still have too many, removing one
|
||||
item.count -= 1;
|
||||
deltaCount -= 1;
|
||||
} else if (deltaCount < 0) {
|
||||
// Still have too few, adding one
|
||||
if (item.count > 0) {
|
||||
item.count += 1;
|
||||
deltaCount += 1;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return versionAndCounts;
|
||||
}
|
||||
|
||||
private async getAgentCount(
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -250,24 +343,14 @@ export class AutomaticAgentUpgradeTask {
|
|||
soClient: SavedObjectsClientContract,
|
||||
agentPolicy: AgentPolicy,
|
||||
requiredVersion: AgentTargetVersion,
|
||||
totalActiveAgents: number
|
||||
versionAndCounts: UpgradeTargetForVersion[]
|
||||
) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: checking candidate agents for upgrade (target version: ${requiredVersion.version}, percentage: ${requiredVersion.percentage})`
|
||||
);
|
||||
|
||||
// Calculate how many agents should meet the version requirement.
|
||||
let numberOfAgentsForUpgrade = Math.round(
|
||||
(totalActiveAgents * requiredVersion.percentage) / 100
|
||||
);
|
||||
// Subtract total number of agents already or on or updating to target version.
|
||||
const updatingToKuery = `(upgrade_details.target_version:${requiredVersion.version} AND NOT upgrade_details.state:UPG_FAILED)`;
|
||||
const totalOnOrUpdatingToTargetVersionAgents = await this.getAgentCount(
|
||||
esClient,
|
||||
soClient,
|
||||
`policy_id:${agentPolicy.id} AND (agent.version:${requiredVersion.version} OR ${updatingToKuery})`
|
||||
);
|
||||
numberOfAgentsForUpgrade -= totalOnOrUpdatingToTargetVersionAgents;
|
||||
let numberOfAgentsForUpgrade =
|
||||
versionAndCounts.find((item) => item.version === requiredVersion.version)?.count ?? 0;
|
||||
// Return if target is already met.
|
||||
if (numberOfAgentsForUpgrade <= 0) {
|
||||
this.logger.info(
|
||||
|
@ -283,6 +366,7 @@ export class AutomaticAgentUpgradeTask {
|
|||
agentPolicy,
|
||||
requiredVersion.version
|
||||
);
|
||||
|
||||
numberOfAgentsForUpgrade -= numberOfRetriedAgents;
|
||||
if (numberOfAgentsForUpgrade <= 0) {
|
||||
return;
|
||||
|
@ -366,6 +450,7 @@ export class AutomaticAgentUpgradeTask {
|
|||
this.logger.info(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: retrying upgrade to ${version} for ${agentsReadyForRetry.length} agents`
|
||||
);
|
||||
|
||||
await sendAutomaticUpgradeAgentsActions(soClient, esClient, {
|
||||
agents: agentsReadyForRetry,
|
||||
version,
|
||||
|
|
|
@ -615,6 +615,8 @@ export const GetActionStatusResponseSchema = schema.object({
|
|||
items: schema.arrayOf(
|
||||
schema.object({
|
||||
actionId: schema.string(),
|
||||
is_automatic: schema.maybe(schema.boolean()),
|
||||
|
||||
nbAgentsActionCreated: schema.number({
|
||||
meta: {
|
||||
description: 'number of agents included in action from kibana',
|
||||
|
|
|
@ -72,6 +72,7 @@ export async function cleanFleetAgentPolicies(esClient: Client) {
|
|||
index: AGENT_POLICY_INDEX,
|
||||
q: '*',
|
||||
refresh: true,
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue